Skip to content
rest-rpc
Esc
navigateopen⌘Jpreview
On this page

Serialization and Codecs

Default body codecs and custom serialization for clients and server adapters

HTTP values travel through URL strings, headers, or request and response bodies. These rules are shared across routes, clients, and server adapters. See the Route Builder for declaring the interface and Schemas and Validation for validation boundaries.

Path, Query, and Headers

The client stringifies scalar values. The server passes HTTP strings to request schemas, which perform any required coercion.

Location Client value shape Wire representation
Path params Object of string, number, or boolean values Each value is URL-encoded into its path segment
Query Object of scalars, scalar arrays, or optional values Scalars use key=value. Arrays use repeated key[]=value
Headers Object of scalars or optional values String values under lowercase header names

Undefined query and header values are omitted. Missing required path params fail on the client. Nested query objects are unsupported. A flat GET .input() uses the same query encoding as .query().

The server reconstructs tag[]=one&tag[]=two as { tag: ["one", "two"] }. A single tag[]=one remains an array. Repeated scalar keys such as tag=one&tag=two keep the last value.

Declare header schemas with lowercase names. Incoming Node and Fetch header names are normalized to lowercase and uppercase schema keys do not match them. Declare request content types through body options rather than a content-type request header.

Bodies and Content Types

Body codecs serialize values into HTTP bodies and deserialize HTTP bodies into values. .input(), .body(), .output(), and .response() default to application/json. Specifying the contentType option can select another media type. Schemas operate on values before encoding and after decoding.

Outgoing codecs are selected from the declared content type. Incoming codecs are selected from the received Content-Type header. Matching uses the lowercase base media type without parameters, so application/json; charset=utf-8 matches the JSON codec. A client response without a content-type header or body yields undefined.

For a list of request content types, select one in the client’s per-call options:

await client.files.upload(bytes, { contentType: "image/png" });

For a list of response content types, the handler selects one with its returned contentType. The schema and codecs must agree on the values represented by each selected type.

Streaming

Async iterable outputs use application/x-ndjson: each item is JSON-encoded on its own line. The client incrementally parses those lines and yields items through an async iterable.

Inferred streams, .streamOutput(), and .streamResponse() use the same framing. Streams bypass bodyCodecs. Declared item schemas validate on the server and optionally on the client with validateResponses.

Fetch Client Defaults

Client Request Serialization

Content type In: client value Out: body on the wire
application/json or any +json media type JSON value UTF-8 JSON text
application/x-www-form-urlencoded URLSearchParams URL-encoded form
multipart/form-data FormData Multipart form. Fetch supplies the boundary and content-type header
text/* string Text
Any other media type Blob or Uint8Array Binary body

Client Response Deserialization

Content type In: response body Out: client value
application/json or any +json media type JSON Parsed JSON value from Response.json()
application/x-www-form-urlencoded URL-encoded form URLSearchParams from Response.text()
multipart/form-data Multipart form FormData from Response.formData()
text/* Text string from Response.text()
Any other media type Binary body Blob from Response.blob()

Client response validation runs after deserialization when validateResponses is enabled. See Schemas and Validation.

Server Adapter Defaults

Servers deserialize incoming request bodies before request validation and serialize outgoing response bodies after response validation.

Server Response Serialization

Adapter Content type In: handler value Out: body on the wire
All adapters application/json or any +json media type JSON value UTF-8 JSON text
All adapters application/x-www-form-urlencoded URLSearchParams URL-encoded form
Fetch, Hono multipart/form-data FormData Multipart form. Fetch supplies the boundary and content-type header
All adapters text/* string UTF-8 text
Fetch, Hono Any other media type Blob or Uint8Array Binary body
Node HTTP, Express, Fastify, NestJS Any other media type Blob, Uint8Array (including Buffer), or Node Readable Binary body. Native values are sent using the adapter’s response API

Binary fallback serializers reject other values. Use a custom serializer to supply another body type supported by the adapter.

Server Request Deserialization

Adapter Content type In: request body Out: value for validation
Fetch, Hono, Node HTTP application/json or any +json media type JSON Parsed JSON value
Fetch, Hono, Node HTTP application/x-www-form-urlencoded URL-encoded form URLSearchParams
Fetch, Hono, Node HTTP multipart/form-data Multipart form FormData
Fetch, Hono, Node HTTP text/* Text string
Fetch, Hono, Node HTTP Any other media type Binary body Blob
Express Types handled by configured middleware Body parsed by Express middleware, such as express.json() req.body
Fastify Types handled by configured content-type parsers Body parsed by Fastify request.body
NestJS Types handled by the underlying Express or Fastify adapter Body parsed by the underlying framework The request’s body value

For Express, Fastify, and Nest, configure the framework’s request parsers or supply a custom deserializer for the content types your routes accept.

Fetch, Hono, and Node limit built-in request decoding to 1 MiB by default. Set requestBodyLimit to change the limit. Malformed bodies returns 400 Response and oversized bodies return 413 Response.

Override A Codec

Pass bodyCodecs to initClient(), registerRoutes(), createRouteHandler(), or RestRpcModule.forRoot(). Each codec has a match(mediaType) function and optional serialize and deserialize functions. Custom codecs run before the defaults. The first matching implementation wins independently for each operation, so overriding only serialization keeps the default deserializer. Both operations can be asynchronous.

For example, this codec uses Uint8Array values for binary requests and responses:

import { initClient, type BodyCodec } from "@rest-rpc/core";
import { createRouteHandler } from "@rest-rpc/fetch";
import { api } from "./contract";
import { routes } from "./routes";

const binaryCodec: BodyCodec<Request | Response> = {
	match: (mediaType) => mediaType === "application/octet-stream",
	serialize: (value) => {
		if (!(value instanceof Uint8Array)) {
			throw new TypeError("Expected Uint8Array body");
		}
		return { body: value };
	},
	deserialize: async (source) => new Uint8Array(await source.arrayBuffer()),
};

const client = initClient(api, {
	baseUrl: "https://api.example.com",
	bodyCodecs: [binaryCodec],
});

const handler = createRouteHandler(routes, {
	bodyCodecs: [binaryCodec],
});

Use schemas that accept the decoded values, and configure compatible codecs on both ends. For other server adapters, adapt deserialize to the native request source listed below and return a serialized body supported by that adapter’s response API.

Custom Server Serializer Delivery

The adapter applies the response status and headers, then delivers serialized.body as follows. A custom serializer must return a value accepted by this delivery path. Its result does not go through the default serializers.

Adapter What happens to serialized.body Requirements for custom serializers
Fetch, Hono Creates and returns new Response(serialized.body, { status, headers }) Return a Fetch response body, such as a string, Blob, FormData, byte array, or Web ReadableStream. A plain object must be encoded first.
Node HTTP, Express If the body is a Node Readable, awaits pipeline(serialized.body, res); otherwise calls res.end(serialized.body) on the server response Return a Node Readable to stream, or a string, Buffer, or Uint8Array to end the response.
Fastify Calls reply.send(serialized.body) on the Fastify reply Return a value accepted by reply.send(). Fastify may perform further serialization, for example when given a plain object. Return encoded bytes when the codec must control the exact payload.
NestJS Returns serialized.body from the route interceptor for Nest to send Return a value Nest can handle. Plain objects go through Nest’s serialization. For binary or streamed delivery, return a StreamableFile from your serializer.

Serializer Result

serialize(value, declaredContentType) receives the full selected content type, including parameters, and returns { body, headers?, contentType? }:

  • body must be usable by the target Fetch or server response API.
  • headers adds headers. Declared request or response headers take precedence. Codec headers cannot contain Content-Type, Content-Length, or Transfer-Encoding (case-insensitively).
  • Omitting contentType preserves the declared content type. A string may change parameters but must keep the same base media type. null omits the explicit content-type header, allowing Fetch to supply it for bodies such as FormData.

Custom Deserializer Sources

deserialize(source) returns the value to validate and receives the native source for the configured entry point:

Entry point Source
Fetch client Fetch Response
Fetch server Fetch Request
Hono HonoRequest (c.req)
Node HTTP IncomingMessage
Express Express Request
Fastify FastifyRequest
NestJS Underlying Express or Fastify request, typed as unknown

Was this page helpful?