Skip to Content
API ReferencecreateParser()

createParser(spec)

createApi validates on the client. createParser gives the server the same guarantees from the same endpoint spec: instead of hand-writing a second Zod schema on the server (which silently drifts from the contract), you derive the parsers directly from the router.

import { createParser } from '@routar/core' import { TodoRouter } from '@/remote/services/todo' const createParserFor = createParser(TodoRouter.endpoints.create) // server framework glue — assemble the { path, query, body } envelope yourself const { body } = await createParserFor.parseRequest({ body: await req.json() }) // ^? { title: string; completed: boolean; userId: number } (validated)

Signature

function createParser<TSpec extends EndpointSpec>(spec: TSpec): Parser<TSpec>
ParamTypeDescription
specEndpointSpecAn endpoint spec created by endpoint() — e.g. router.endpoints.create.

Pass a spec created by endpoint(). The conditional parseRequest (below) relies on endpoint()’s return type, where request is a required field. If you hand-annotate a spec as EndpointSpec, request becomes optional (| undefined) and parseRequest silently drops out of the type (though it still runs) — the same endpoint()-return-type contract createApi already depends on.

Return value

createParser returns an object whose shape depends on the spec:

MethodWhen presentDescription
parseResponse(raw: unknown)AlwaysValidates raw against spec.response. Returns Promise<ValidatorOutput<response>>.
parseRequest(raw: { path?, query?, body? })Only when the spec has a requestValidates the request envelope. Returns Promise<R> where R is the composed request output.

Both delegate to the same runValidator the client uses: valid input resolves to the parsed value; invalid input throws the original error (a Zod ZodError, or a StandardSchemaError for Standard Schema validators) unchanged.

parseRequest narrows by the spec

When the endpoint has no request (a plain GET with no params), the returned object has no parseRequest — accessing it is a compile error:

// a plain GET with no `request` in its spec const ping = createParser(endpoint({ method: 'GET', path: '/', response: PingSchema })) ping.parseResponse(data) // ✅ ping.parseRequest({}) // ❌ compile error — property does not exist

When the endpoint has a request, parseRequest is present and typed. You assemble the { path, query, body } envelope from your framework’s request object — routar has no opinion about how you read it:

const update = createParser(TodoRouter.endpoints.update) const { path, body } = await update.parseRequest({ path: { id: params.id }, // string → coerced by the spec's path schema body: await req.json(), })

No HTTP concerns

createParser does no status-code mapping and no error formatting. Turning a thrown ZodError into a 400/422 is the calling app’s job — which is exactly what makes it framework-agnostic. It works under Next.js Route Handlers, Hono, Express, or anything else:

// Next.js Route Handler try { const { body } = await createParserFor.parseRequest({ body: await req.json() }) return Response.json(createTodo(body), { status: 201 }) } catch (err) { if (err instanceof ZodError) return Response.json({ error: err.flatten() }, { status: 400 }) throw err }
// Hono / Express — same parser, different glue app.post('/todos', async (c) => { const { body } = await createParserFor.parseRequest({ body: await c.req.json() }) return c.json(createTodo(body), 201) })

parseResponse does not run the adapter

parseResponse validates against the pure response schema and does not apply the endpoint’s adapter. Its output is ValidatorOutput<response>, not the adapter output (ApiTypes<…>['response']). The adapter is client-side post-processing — the server’s job is only to confirm it produced data that matches the contract:

const { getDetail } = TodoRouter.endpoints const parser = createParser(getDetail) // validated against the raw response schema — adapter (label, etc.) NOT applied const todo = await parser.parseResponse(rowFromDb)

See also

Last updated on