Skip to content
UQL

Express

pool is a plain ORM, so an Express app needs nothing else from UQL: your own routes query it directly. The middleware below that is optional, a thin adapter over the HTTP transport core for when you want CRUD across many entities without writing it.

import express from 'express';
import { pool } from './uql.config.js';
import { Post } from './shared/models/index.js';
const app = express();
app.get('/posts', async (_req, res) => {
const posts = await pool.findMany(Post, {
$where: { published: true },
$limit: 20,
});
res.json(posts);
});
app.listen(3000);

Work spanning several statements goes in pool.transaction, which hands you the querier to run all of them on.

Requires Express 5, whose route syntax the middleware is written against. NestJS has been on Express 5 since v11, so a current Nest app already qualifies.

import { querierMiddleware } from 'uql-orm/express';
import { User } from './shared/models/index.js';
app.use(express.json()); // the write routes and the QUERY transport need a parsed body
app.use('/api', querierMiddleware({ pool, include: [User, Post] }));

That mounts the full wire protocol per entity, including the QUERY transport. Unknown entities and routes fall through via next(), so your own routes (webhooks, payments, SSE) share the prefix.

:id is not hardcoded: the route parameter maps to whatever property carries @Id(), so uuid or itemNo work unchanged, the latter naming its key, as any unconventional one does.

The core’s hooks apply, with ctx.context bound to the express.Request:

app.use(
'/api',
querierMiddleware({
pool,
include: [User, Post],
async pre({ context }) {
if (!context.user) {
throw Object.assign(new Error('unauthorized'), { status: 401 }); // numeric status becomes the HTTP status
}
},
preSave(ctx) {
ctx.body = { ...(ctx.body as object), creatorId: ctx.context.user?.id };
},
}),
);

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

Errors go to next(err), so your own error middleware keeps working. The exported errorHandler renders the canonical envelope and honors a numeric status thrown by a hook:

import { errorHandler } from 'uql-orm/express';
app.use('/api', querierMiddleware({ pool, include: [User] }));
app.use(errorHandler);