Next.js
Server components, route handlers and server actions run on the server, so they query the pool directly. examples/nextjs is a runnable Next.js 16 app, tested in CI.
npm install uql-orm pg server-onlynpm install -D @babel/plugin-proposal-decorators@7 tsxWhere the pool lives
Section titled “Where the pool lives”import 'server-only';import { PgQuerierPool } from 'uql-orm/postgres';import './entities'; // registers the decorated entities
// hot reload re-runs this module; reuse the pool instead of leaking a new one each timedeclare global { var uqlPool: PgQuerierPool | undefined;}
export const pool = (globalThis.uqlPool ??= new PgQuerierPool({ connectionString: process.env.DATABASE_URL,}));server-only turns an import from a client component into a build error, so the connection string never ships to the browser. tsx is for uql-migrate.
Decorators
Section titled “Decorators”Next’s SWC compiles only legacy decorators, and UQL uses the standard ones. Either let Babel compile them, leaving experimentalDecorators off; Next picks up the config on its own, Turbopack included. Or declare entities with defineEntity, which needs no decorators and so no Babel config:
{ "presets": ["next/babel"], "plugins": [["@babel/plugin-proposal-decorators", { "version": "2023-11" }]]}import { f, defineEntity } from 'uql-orm';
export const User = defineEntity({ name: 'User', fields: { id: f.number().autoIncrement(), email: f.string().unique(), },});export interface User extends InstanceType<typeof User> {}The production build minifies class names, and a table is named after its class by default, so name every table: @Entity({ name: 'users' }) (details).
Server components and route handlers
Section titled “Server components and route handlers”import { pool } from '@/db/uql';import { User } from '@/db/entities';
export default async function UsersPage() { const users = await pool.findMany(User, { $select: { id: true, name: true }, $populate: { posts: { $select: { title: true }, $limit: 5 } }, $limit: 20, });
return ( <ul> {users.map((user) => ( <li key={user.id}> {user.name} ({user.posts.length}) </li> ))} </ul> );}A route handler queries the same way and returns NextResponse.json(rows). Keep it on the default Node.js runtime, since pg needs TCP.
Server actions
Section titled “Server actions”An action is a public endpoint, so build the query from validated input, never from a client’s Query<User>. Writes that must land together go in pool.transaction:
'use server';
import { revalidatePath } from 'next/cache';import { z } from 'zod';import { pool } from '@/db/uql';import { Post, User } from '@/db/entities';
const NewUser = z.object({ email: z.email(), name: z.string().trim().min(1) });
export async function createUser(formData: FormData) { const form = NewUser.safeParse(Object.fromEntries(formData)); if (!form.success) return { errors: z.flattenError(form.error).fieldErrors };
await pool.transaction(async (querier) => { const id = await querier.insertOne(User, form.data); await querier.insertOne(Post, { authorId: id, title: 'Hello' }); }); revalidatePath('/users');}Auto-generated CRUD
Section titled “Auto-generated CRUD”Mount the HTTP handler in an optional catch-all route, with the prefix as basePath:
import { createFetchHandler } from 'uql-orm/http';import { pool } from '@/db/uql';import { User } from '@/db/entities';
const handler = createFetchHandler({ pool, include: [User], basePath: '/api/uql',});
export { handler as GET, handler as HEAD, handler as POST, handler as PUT, handler as PATCH, handler as DELETE,};Route handlers export the standard verbs only, so keep the browser client on GET.
Multi-tenancy
Section titled “Multi-tenancy”Wrap the work in withContext with the tenant from a verified session. A security filter then scopes each query inside, across every await:
import 'server-only';import { withContext } from 'uql-orm';import { getSession } from '@/auth';
export async function withTenant<T>(run: () => Promise<T>): Promise<T> { const session = await getSession(); return session ? withContext({ tenantId: session.tenantId }, run) : run();}Then query inside it: withTenant(() => pool.findMany(Invoice, { $limit: 50 })). Next’s proxy.ts cannot set the context, since it returns before your page queries. The CRUD route takes getContext instead. See Multi-tenancy.