Server

JSON Round Trip

How mion encodes params and return values, and how to pick a strategy per route.

mion runs the JSON round trip with @mionjs/run-types. You work with native types (Dates, BigInts, Sets, Maps and more) and mion converts them on both ends, with no extra configuration.

import {createMionRouter, Routes} from '@mionjs/router';

const mion = createMionRouter();

// Your TypeScript types ARE the validation schema
interface User {
  id: string;
  email: string;
  age: number;
  birthDate: Date;
  tags: Set<string>;
}

type NewUser = Omit<User, 'id'>;

const routes = {
  createUser: mion.route((ctx, user: NewUser): User => {
    // Date and Set were restored from JSON, then user was validated
    console.log(user.birthDate instanceof Date); // true
    console.log(user.tags instanceof Set); // true
    return {id: 'USER-123', ...user};
  }),
} satisfies Routes;

Parser Strategies

Every route has two wires: the params (the client encodes, the server decodes) and the return value (the server encodes, the client decodes). The parser option picks a strategy for each one.

import {createMionRouter} from '@mionjs/router';

// no parser option: `clone` on both directions, so only what the types declare reaches the wire
export const defaultMion = createMionRouter({basePath: 'api'});

// one strategy for both directions
export const compactMion = createMionRouter({parser: 'compact'});

// or one strategy per direction
export const mixedMion = createMionRouter({
  parser: {params: 'compact', return: 'mutate'},
});

// mutateStrict refuses a request carrying a property the params types do not declare
export const strictMion = createMionRouter({parser: {params: 'mutateStrict'}});
StrategyWhat it doesMutates your objectsUndeclared params propertiesPayload
cloneBuilds a fresh JSON value from the type, then the platform stringifies it.NoDroppedNormal
mutateRewrites Dates, Maps and the like in place, then the platform stringifies the value.YesKeptNormal
mutateStrictSame as mutate, and rejects the request when a property is undeclared.YesRejectedNormal
compactDrops the key names: an object becomes an array of its declared properties, in order.NoDroppedSmallest

Undeclared properties matter most on params, because a caller that is not a mion client can send them. mutateStrict is params-only: a return value has no caller to reject.

A dropped property is gone from the params, not set to undefined, so it cannot slip into a database insert. Every strategy also checks unions, where dropping is not enough: see Undeclared Properties.

Sending Only What the Type Declares

With clone and compact, the return type decides the payload, not the object the handler returns. Ten database columns come out, three go on the wire, and the password hash cannot leak by accident.

  // returns the whole row, but the PublicUser return type decides what is sent
  getUser: mion.route((ctx, id: string): PublicUser => db.users.byId(id)),

Sending the Returned Object As Is

mutate is the one strategy that sends the returned object as it is.

  // `mutate` sends the object as is, so this handler builds exactly what the type declares
  getUserFast: mion.route(
    (ctx, id: string): PublicUser => {
      const row = db.users.byId(id);
      return {id: row.id, name: row.name, avatarUrl: row.avatarUrl};
    },
    {parser: 'mutate'}
  ),
mutate is the faster return strategy: it rewrites Dates and the like in place and allocates no copy. It is safe when the handler is done with the object it returns, and the object holds exactly what the type declares.

Choosing a Strategy Per Route

A route or middleware can override the router's parser, for one direction or both. Give a middleware that takes params or returns data the same strategy when its route uses compact.

import {createMionRouter, Routes} from '@mionjs/router';

const mion = createMionRouter({basePath: 'api'});

interface Measurement {
  sensorId: string;
  samples: number[];
}

// a preset shared by several routes, `as const` so the build reads it like an inline literal
const compactRoute = {
  parser: 'compact',
  description: 'positional wire',
} as const;

export const routes = {
  // the router default: clone on both directions
  echo: mion.route((ctx, data: Measurement): Measurement => data),

  // compact on both directions, for this route only
  echoCompact: mion.route((ctx, data: Measurement): Measurement => data, {
    parser: 'compact',
  }),

  // only the return changes; the params keep the router default
  echoMutate: mion.route((ctx, data: Measurement): Measurement => data, {
    parser: {return: 'mutate'},
  }),

  // refuse a request carrying a property Measurement does not declare
  echoStrict: mion.route((ctx, data: Measurement): Measurement => data, {
    parser: {params: 'mutateStrict'},
  }),

  // the shared preset
  echoPreset: mion.route(
    (ctx, data: Measurement): Measurement => data,
    compactRoute
  ),

  // middleware takes the same option: its params and return use the same wires
  stamp: mion.middleware(
    (ctx, tag: string): {tag: string; at: Date} => ({tag, at: new Date()}),
    {parser: 'compact'}
  ),
} satisfies Routes;
Write the parser option inline or as an as const preset, because the build reads it. A value built at run time (a function call, a variable typed string) is a build error and a lint error, and the router refuses to start when the value it reads differs from what was compiled.

What Each Route Compiles

A route only compiles the functions its strategies need: one validator and one error reporter per direction, plus one encode and decode pair. The default ships the clone pair, compact the positional one, and the params validator follows the strategy too. Every strategy hands the platform a value to stringify, so the response framing is the same whichever one you pick.

Compact Payloads

A compact payload has no keys, so another service cannot read it by key. Pick clone for an endpoint other tools consume. The payload sizes benchmark compares compact with plain JSON, in bytes and speed.

  • Both ends must share the same type, which mion guarantees by sending the compiled functions to the client.
  • A missing optional property is sent as the first of 0, null or false its type cannot hold, and comes back undefined. So s?: string | null keeps a present null. An optional that can hold all three, like any or unknown, reads a present null as missing.
  • A union of objects is sent as one array holding every member's properties. Each value is wrapped with its member index only when another member is not a plain string, number, boolean or null.
Copyright © 2026