Middlewares

security

Stop other sites from acting with your users' cookies, and let chosen origins call your API.

Stop other sites from sending requests with your users' cookies (CSRF, cross-site request forgery). Let the browser apps you choose call your API from another origin (CORS, cross-origin resource sharing).

Add it whenever the browser attaches a login on its own, like a cookie. With a login in an Authorization header, you need it only for CORS.

Adding the Middleware

Add mionSecurity from @mionjs/router/middlewares, first. A preflight (the browser's check request before a cross-origin call) carries none of your headers, so an auth middleware before it would refuse it.

server.routes.ts
import {HeadersSubset} from '@mionjs/core';
import {createMionRouter, Routes} from '@mionjs/router';
import {mionSecurity, mionFetchMetadata} from '@mionjs/router/middlewares';

const mion = createMionRouter();

type User = {name: string; surname: string};

const routes = {
  // first, before the auth middleware
  mionSecurity: mionSecurity({
    origins: ['https://app.example.com'],
    cors: {credentials: true, maxAge: 600},
  }),
  mionFetchMetadata,
  auth: mion.headersMiddleware(
    (ctx, {headers}: HeadersSubset<'Authorization'>): void => undefined
  ),
  // `x-ratelimit-remaining` is exposed to the browser with no CORS setting
  rateLimit: mion.middleware(
    (ctx): HeadersSubset<'x-ratelimit-remaining'> =>
      new HeadersSubset({'x-ratelimit-remaining': '99'})
  ),
  listUsers: mion.query((ctx): User[] => [{name: 'John', surname: 'Doe'}]),
} satisfies Routes;

const myApi = mion.initRoutes(routes);

export type MyApi = typeof myApi;
At the root, mionSecurity covers every route. Inside a group, it covers only that group.

Security Options

server.routes.ts
import {HeadersSubset} from '@mionjs/core';
import {createMionRouter, Routes} from '@mionjs/router';
import {mionSecurity} from '@mionjs/router/middlewares';

const mion = createMionRouter();

const routes = {
  mionSecurity: mionSecurity({
    // asked only when a request's origin matters; may be async, like a lookup in your database
    origins: async (ctx, origin) => origin.endsWith('.example.com'),
    cors: {
      // a raw middleware reads and sets these by hand, so no type declares them
      allowHeaders: ['x-request-id'],
      exposeHeaders: ['x-response-time'],
      // declared below, still sent, but a browser on another origin cannot read it
      excludeHeaders: ['x-served-by'],
    },
  }),
  timing: mion.rawMiddleware((ctx): void => {
    ctx.response.headers.set('x-response-time', '3ms');
  }),
  servedBy: mion.middleware(
    (ctx): HeadersSubset<'x-served-by'> =>
      new HeadersSubset({'x-served-by': 'eu-1'})
  ),
} satisfies Routes;
OptionWhat it does
originsOrigins you trust: a list, '*' for any, or a function (ctx, origin) returning a boolean. They may send requests from another site and, with cors, read the answers. null is never trusted.
corsLets the trusted origins read answers. Off by default. Needs origins, or it throws.
cors.credentialsLets the browser send cookies. Needs listed origins: '*' with credentials throws.
cors.maxAgeSeconds the browser keeps a preflight answer before asking again.
cors.allowHeadersExtra request headers a raw middleware reads.
cors.exposeHeadersExtra response headers a raw middleware sets.
cors.excludeHeadersHeaders the browser never reads or sends cross-origin, even when a route declares them. Wins over allowHeaders and exposeHeaders.
csrfThe checks below, all on by default. csrf: false turns them all off.

CSRF Checks

Each check refuses a request another site can make a browser send. A call from the mion client always passes.

server.routes.ts
import {createMionRouter, Routes} from '@mionjs/router';
import {mionSecurity} from '@mionjs/router/middlewares';

const mion = createMionRouter();

const routes = {
  // no options: same-origin browsers and non-browser callers pass, CORS is off
  mionSecurity: mionSecurity(),
  getUser: mion.query((ctx, id: number): string => `user ${id}`),
  // POST only: a link or an image on another site never runs it
  deleteUser: mion.mutation((ctx, id: number): boolean => true),
} satisfies Routes;
CheckRefusesAnswer
methodByIntentA method the route's intent does not take: a query takes GET, HEAD and POST; a mutation, a route and a batch take only POST.405 method-not-allowed, with an Allow header
crossOriginA POST another site sent, unless its origin is trusted. Read from the browser's Sec-Fetch-Site header, else from Origin against Host. A caller sending neither (a server, a script, curl) passes.403 cross-origin-request-refused
JSON body, no option of its ownA body not sent as application/json, like a plain HTML form or a fetch with no type.415 unsupported-media-type

CORS Headers From Your Types

mionSecurity fills the CORS headers from your routes' types. Adding a header to a route needs no change here.

CORS headerHolds
Access-Control-Allow-Headerscontent-type, every header a headers middleware reads, and allowHeaders
Access-Control-Expose-HeadersEvery header a route or middleware returns, mion's x-rpc-error and x-build-version, and exposeHeaders
Access-Control-Allow-MethodsGET, POST for a query, POST for the rest
Add cors: false to a route or middleware to keep all its headers off both lists, like excludeHeaders. It needs MionSecurityHandlerOptions in your router's handler part, see Custom Router Types.

Answers per Request

RequestAnswer
From a trusted originThe route runs; with cors, the answer allows the origin and lists the exposed headers.
A GET from another originThe route runs, with no CORS headers, so the browser hides the answer.
A POST from another originRefused with a 403.
A preflight (OPTIONS)Answered 200 by the middleware, CORS on or off. The route never runs.
A refused request or an errorThe same CORS headers, so the client can read the error.
Every answerx-content-type-options: nosniff, and no server: mionjs header. A server value you set in responseHeaders stays.
A request whose body length is unclear: a repeated or non-numeric content-length, or a transfer-encoding other than chunkedRefused with a 400 unclear-body-length, and the connection is closed.
An answer that depends on request headersVary lists every header a headers middleware reads, plus Origin with cors. A shared cache then never gives one user's answer to another.

What the Middleware Protects

The CSRF checks stop other sites from making the browser act for your users. CORS decides which pages may read your answers. Neither is access control: a server, a script or curl calls your API with no browser. Keep your auth middleware.

An excluded header is still sent, so never use excludeHeaders to hide a secret.

What Your Server or Proxy Handles

mion does not handle these. Set them up in your server, your proxy or the platform in front.

ConcernWhere to handle it
HTTPS and Strict-Transport-SecurityYour proxy, or the router's responseHeaders option.
Rate limits and request floodsYour proxy, API gateway or platform. One server process cannot count requests across instances.
Slow clients and timeoutsThe server options of your platform, or your proxy.
Cookie flags (Secure, HttpOnly, SameSite)The middleware that sets the cookie.
Caching rules (Cache-Control)A route's headers, or responseHeaders with no-store for every answer.
Proxy headers (X-Forwarded-For, X-Forwarded-Host)Your proxy. mion never reads them.
Copyright © 2026