TypeScript REST Server
The typescript-rest target can generate a client, a server contract, or both.
The generated server contract contains service interfaces and operation metadata;
request parsing, validation, response encoding, and framework integration live in
the reusable xidl-typescript-server runtime.
Generate Client and Server Artifacts
Section titled “Generate Client and Server Artifacts”Without flags, typescript-rest generates the client only:
xidlc gen -o src/generated typescript-rest api.idlGenerate only the server contract:
xidlc gen -o src/generated typescript-rest --server api.idlGenerate both client and server artifacts:
xidlc gen -o src/generated typescript-rest --client --server api.idlThe important outputs are:
api.client.tswhen client generation is enabled.api.server.tswhen server generation is enabled.- Shared TypeScript model, interface, and Zod schema files used by both sides.
Install the Runtime
Section titled “Install the Runtime”pnpm add xidl-typescript-server xidl-typescript-client xidl-typescript-codec zodWhen you generate the client only, xidl-typescript-client provides the
Fetch API client runtime (request building, response decoding, SSE streams,
and auth). xidl-typescript-server uses the standard Fetch API Request and Response
types, so the service implementation is independent of a particular Node.js
framework. The Next.js App Router adapter is available from the
xidl-typescript-server/next subpath.
Implement the Generated Service
Section titled “Implement the Generated Service”Assume the IDL contains this service:
struct User { uint32 id; string name;};
interface UserService { @get(path = "/api/users/{id}") User get_user(@path uint32 id);
@post(path = "/api/users") User create_user(User user);};Implement the interface generated in api.server.ts once, and share it across
all routers and route handlers:
import { XidlServerError } from 'xidl-typescript-server';import type { User } from './generated/api';import type { UserService } from './generated/api.server';
export class AppUserService implements UserService { private readonly users = new Map<number, User>();
async get_user(id: number): Promise<User> { const user = this.users.get(id); if (!user) { throw new XidlServerError(404, 'user not found'); } return user; }
async create_user(user: User): Promise<User> { this.users.set(user.id, user); return user; }}
export const userService = new AppUserService();Generated methods receive one positional argument per IDL parameter, in declaration order. Path, query, header, cookie, and body values are extracted and validated by the runtime before the service method runs.
Use the Fetch API Router
Section titled “Use the Fetch API Router”createRouter combines operation metadata with a service implementation and
returns a Fetch API-compatible handler:
import { createRouter } from 'xidl-typescript-server';import { UserServiceOperations } from './generated/api.server';import { userService } from './service';
export const handler = createRouter( Object.values(UserServiceOperations), userService,);
export function fetch(request: Request): Promise<Response> { return handler(request);}Attach this handler to the adapter provided by the target runtime. The router handles path matching, query/header/cookie/body extraction, Zod validation, content negotiation, and response serialization.
Integrate with Next.js App Router
Section titled “Integrate with Next.js App Router”Next.js Route Handlers export functions named after HTTP methods from a
route.ts file. createNextRoute adapts one generated operation directly to
that convention.
For POST /api/users, create app/api/users/route.ts:
import { createNextRoute } from 'xidl-typescript-server/next';import { UserServiceOperations } from '../../../src/generated/api.server';import { userService } from '../../../src/service';
export const POST = createNextRoute( UserServiceOperations.create_user, userService,);For GET /api/users/{id}, create app/api/users/[id]/route.ts:
import { createNextRoute } from 'xidl-typescript-server/next';import { UserServiceOperations } from '../../../../src/generated/api.server';import { userService } from '../../../../src/service';
export const GET = createNextRoute( UserServiceOperations.get_user, userService,);The dynamic segment name must match the IDL route parameter: [id] maps to
{id}. In current Next.js versions, Route Handler parameters are promises;
createNextRoute awaits context.params internally before invoking the
service. The same pattern works for PUT, PATCH, and DELETE when the
generated operation uses that method.
Route Handler Semantics
Section titled “Route Handler Semantics”createNextRoute returns a handler with the signature
(request: Request, context: { params }) => Promise<Response>, matching the
Next.js Route Handler contract:
- Path, query, header, cookie, and body values are extracted, coerced, and validated against the generated Zod schema before the service method runs.
- Invalid requests produce HTTP 400 responses, unsupported media types produce 415/406, and unhandled service errors produce HTTP 500.
- Return the handler as a named export from
route.ts; never call it manually.
Errors, Authorization, and Codecs
Section titled “Errors, Authorization, and Codecs”Pass ServerOptions as the optional third argument of createRouter or
createNextRoute:
export const POST = createNextRoute( UserServiceOperations.create_user, userService, { authorize: async (request, requirements) => { // inspect headers/cookies, throw XidlServerError to reject }, onError: async (error, context) => { // map unexpected errors to a custom response }, codecs: { 'application/msgpack': { decode: async (response, schema) => { /* ... */ }, encode: (value, schema) => { /* ... */ }, }, }, },);- Throw
XidlServerError(code, msg)from the service for an explicit HTTP status and typed JSON error body. The HTTP status is set fromcode. - Invalid generated request schemas return HTTP 400 responses.
- Unsupported request and response media types return HTTP 415 and 406.
authorizereceives the original request, the operation’s security requirements, and aServerContext; throw to reject the request.codecsoverrides request/response encoding per media type.onErrorhandles unexpected errors, for example to add logging; by default errors that are notXidlServerErrorproduce HTTP 500 withmsgset toString(error).
Streaming
Section titled “Streaming”Operations declared with streaming media types are handled by the runtime according to the generated operation metadata, with no per-operation code in the service or route handler:
- SSE server streams and NDJSON client streams work through the same
createRouter/createNextRoutehandlers. sequence<octet>mapped to raw byte streams is served asResponsebodies.