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 { pool } from './uql.config.js';import { Post, User } from './shared/models/index.js';
const fastify = Fastify();const handle = createRequestHandler({ pool, 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.
Your own routes
Section titled “Your own routes”The bridge is optional. pool is a plain ORM, so any route can query it:
fastify.get('/posts', () => pool.findMany(Post, { $where: { published: true }, $limit: 20 }),);Work spanning several statements goes in pool.transaction, which hands you the querier to run all of them on.
createRequestHandler accepts the core’s hooks; the hook context is whatever you passed above, here the Fastify request:
const handle = createRequestHandler({ pool, 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.