Skip to content

Fastify

Fastify is not fetch-native, so it binds the HTTP transport core through createRequestHandler, which takes a normalized request object and returns { status, body }. The bridge is one catch-all route.

import Fastify from 'fastify';
import { createRequestHandler, toErrorResponse } from 'uql-orm/http';
import './uql.config.js'; // setQuerierPool + entity imports
import { Post, User } from './shared/models/index.js';
const fastify = Fastify();
const handle = createRequestHandler({ include: [User, Post] });
fastify.all<{
Params: { entityPath: string; subPath?: string };
Querystring: Record<string, unknown>;
}>('/api/:entityPath/:subPath?', async (req, reply) => {
const { entityPath, subPath } = req.params;
const pending = handle({
method: req.method,
entityPath,
subPath,
query: req.query,
body: req.body,
context: req, // passed through to hooks
});
// unknown entity/route: hand it to Fastify's not-found handler
if (!pending) return reply.callNotFound();
try {
const { status, body } = await pending;
return reply.status(status).send(body);
} catch (err) {
const { status, body } = toErrorResponse(err);
return reply.status(status).send(body);
}
});
await fastify.listen({ port: 3000 });

That serves the full wire protocol per entity. Thrown hook errors map to the canonical envelope via toErrorResponse, with a numeric status on the error becoming the HTTP status. Fastify already parses application/json, so the write routes need nothing added.

fastify.all registers the standard verbs only, so this bridge serves the GET read transport but not the QUERY method.

createRequestHandler accepts the core’s hooks; the hook context is whatever you passed above, here the Fastify request:

const handle = createRequestHandler({
include: [User, Post],
async pre({ context }) {
if (!context.user) {
throw Object.assign(new Error('unauthorized'), { status: 401 });
}
},
});

For tenant isolation pass getContext plus a security filter rather than folding $where by hand: it scopes every query in the request, cannot be bypassed from the wire, and fails closed. See Multi-tenancy.

Register your own routes on their own paths and Fastify matches them before this catch-all, so both styles coexist.