Skip to content

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.

Without flags, typescript-rest generates the client only:

Terminal window
xidlc gen -o src/generated typescript-rest api.idl

Generate only the server contract:

Terminal window
xidlc gen -o src/generated typescript-rest --server api.idl

Generate both client and server artifacts:

Terminal window
xidlc gen -o src/generated typescript-rest --client --server api.idl

The important outputs are:

  • api.client.ts when client generation is enabled.
  • api.server.ts when server generation is enabled.
  • Shared TypeScript model, interface, and Zod schema files used by both sides.
Terminal window
pnpm add xidl-typescript-server xidl-typescript-client xidl-typescript-codec zod

When 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.

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.

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.

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.

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.

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 from code.
  • Invalid generated request schemas return HTTP 400 responses.
  • Unsupported request and response media types return HTTP 415 and 406.
  • authorize receives the original request, the operation’s security requirements, and a ServerContext; throw to reject the request.
  • codecs overrides request/response encoding per media type.
  • onError handles unexpected errors, for example to add logging; by default errors that are not XidlServerError produce HTTP 500 with msg set to String(error).

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 / createNextRoute handlers.
  • sequence<octet> mapped to raw byte streams is served as Response bodies.