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

Custom Bodies

Model non-JSON request and response bodies.

Default JSON object request bodies are flattened into handler and client input.

Use customBody() when the body should be treated as one whole value instead or when the body is not JSON.

import { customBody } from "@rest-rpc/core";

request: {
	params: {
		id: z.string(),
	},
	body: customBody({
		contentType: "image/png",
		schema: z.instanceof(Uint8Array),
	}),
}

The handler and client shape is:

{
	id: string;
	body: Uint8Array;
}

Request Bodies

export const api = router({
	images: {
		upload: {
			method: "PUT",
			path: "/images/:id",
			request: {
				params: {
					id: z.string(),
				},
				body: customBody({
					contentType: "image/png",
					schema: z.instanceof(Uint8Array),
				}),
			},
			responses: {
				204: noBody(),
			},
		},
	},
});
upload({ id, body }) {
	saveImage(id, body);
	return undefined;
}
await client.images.upload.fetch({
	id: "image_1",
	body: new Uint8Array(await file.arrayBuffer()),
});

Response Bodies

Use customBody() in responses when the response body has a specific content type.

download: {
	method: "GET",
	path: "/images/:id",
	request: {
		params: {
			id: z.string(),
		},
	},
	responses: {
		200: customBody({
			contentType: "image/png",
			schema: z.instanceof(Uint8Array),
		}),
		404: z.object({
			code: z.literal("IMAGE_NOT_FOUND"),
		}),
	},
}

On the client, a custom response body exposes the raw Response object.

const response = await client.images.download.fetch({
	id: "image_1",
});

const bytes = await response.arrayBuffer();

Body Parsing

Server adapters do not infer your intended body parsing.

For Express, choose middleware before route execution.

import { isCustomBody } from "@rest-rpc/core";
import { createRouteMatcher } from "@rest-rpc/express";

const matchContractRoute = createRouteMatcher(api);

const jsonParser = express.json();
const pngParser = express.raw({ type: "image/png" });

app.use((req, res, next) => {
	const match = matchContractRoute({
		method: req.method,
		path: req.path,
	});
	const body = match?.route.request?.body;

	if (isCustomBody(body) && body.contentType === "image/png") {
		return pngParser(req, res, next);
	}

	return jsonParser(req, res, next);
});

For Hono, use parseBody.

registerRoutes(app, routes, {
	parseBody: async ({ c, body }) => {
		if (isCustomBody(body)) {
			return c.req.arrayBuffer();
		}

		return c.req.json();
	},
});

Was this page helpful?