| title | Custom Server Adapter |
|---|---|
| description | Wire ZenStack API handlers into frameworks that do not have a built-in adapter yet. |
| sidebar_position | 20 |
Server adapters translate framework-specific requests into the framework-agnostic contract implemented by ZenStack API handlers. If your runtime is not covered by a built-in adapter, you can create a lightweight bridge by combining the shared adapter utilities and the generic handler contract.
import { logInternalError, type CommonAdapterOptions } from '@zenstackhq/server/common';
import type { ApiHandler, RequestContext, Response } from '@zenstackhq/server/types';CommonAdapterOptionsgives every adapter anapiHandlerfield so it can delegate work to REST, RPC, or a custom handler.logInternalErrormirrors the logging behavior of the official adapters, making it easy to surface unexpected failures.ApiHandler,RequestContext, andResponsedescribe the shape of the data you must provide to the handler and how to forward the result back to your framework.
- Identify the minimal options surface. Extend
CommonAdapterOptionswith whatever context your framework needs (for example, agetClientcallback or a URL prefix). - Map the framework request to
RequestContext. Collect the HTTP method, path (excluding any prefix), query parameters, body, and the ZenStack client instance. Move the heavy lifting—policy enforcement, serialization, pagination—to the handler. - Send the handler response back through the framework. Serialize
Response.body, apply the status code, and fall back tologInternalErrorif anything throws.
The snippet below wires IncomingMessage/ServerResponse from Node's http module into any ZenStack handler.
import type { IncomingMessage, ServerResponse } from 'http';
import type { ClientContract } from '@zenstackhq/orm';
import type { SchemaDef } from '@zenstackhq/orm/schema';
import { logInternalError, type CommonAdapterOptions } from '@zenstackhq/server/common';
import type { RequestContext } from '@zenstackhq/server/types';
interface NodeAdapterOptions<Schema extends SchemaDef> extends CommonAdapterOptions<Schema> {
prefix?: string;
getClient(request: IncomingMessage, response: ServerResponse): ClientContract<Schema> | Promise<ClientContract<Schema>>;
}
export function createNodeAdapter<Schema extends SchemaDef>(
options: NodeAdapterOptions<Schema>,
): (request: IncomingMessage, response: ServerResponse) => Promise<void> {
const prefix = options.prefix ?? '/api';
return async (request, response) => {
if (!request.url || !request.method || !request.url.startsWith(prefix)) {
response.statusCode = 404;
response.end();
return;
}
let client: ClientContract<Schema> | undefined;
try {
client = await options.getClient(request, response);
} catch (err) {
logInternalError(options.apiHandler.log, err);
}
if (!client) {
response.statusCode = 500;
response.setHeader('content-type', 'application/json');
response.end(JSON.stringify({ message: 'Unable to resolve ZenStack client' }));
return;
}
const url = new URL(request.url, 'http://localhost');
const query = Object.fromEntries(url.searchParams);
const requestBody = await readJson(request);
const context: RequestContext<Schema> = {
method: request.method,
path: url.pathname.slice(prefix.length) || '/',
query,
requestBody,
client,
};
try {
const handlerResponse = await options.apiHandler.handleRequest(context);
response.statusCode = handlerResponse.status;
response.setHeader('content-type', 'application/json');
response.end(JSON.stringify(handlerResponse.body));
} catch (err) {
logInternalError(options.apiHandler.log, err);
response.statusCode = 500;
response.setHeader('content-type', 'application/json');
response.end(JSON.stringify({ message: 'An internal server error occurred' }));
}
};
}
async function readJson(request: IncomingMessage) {
const chunks: Array<Buffer> = [];
for await (const chunk of request) {
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
}
if (chunks.length === 0) {
return undefined;
}
const payload = Buffer.concat(chunks).toString('utf8');
return payload ? JSON.parse(payload) : undefined;
}You can plug the adapter into a server just like the packaged adapters:
import { createServer } from 'http';
import { RestApiHandler } from '@zenstackhq/server/api';
import { schema } from '~/zenstack/schema';
import { createNodeAdapter } from './node-adapter';
const handler = new RestApiHandler({ schema, endpoint: 'https://api.example.com' });
createServer(
createNodeAdapter({
prefix: '/api',
apiHandler: handler,
getClient: (req, res) => /* return a tenant-aware ZenStack client based on req */,
}),
).listen(3000);- Review the implementation of a built-in adapter—such as Express or SvelteKit—for inspiration on error handling, streaming bodies, and auth integration.
- Pair a custom adapter with an extended handler from Custom API Handler to keep framework and business logic responsibilities cleanly separated.