> Every UQL docs page, as Markdown: https://uql-orm.dev/llms.txt
> The same docs over MCP: https://uql-orm.dev/mcp
> Before writing UQL code, read the skill: https://uql-orm.dev/.well-known/agent-skills/uql-orm/SKILL.md

# Next.js

> Use UQL in Next.js App Router server components, route handlers, and server actions.

Source: https://uql-orm.dev/nextjs

Anything that runs on the server in the App Router can query directly: a server component, a route handler, a server action. No adapter for any of them. Verified against Next.js 16, where Turbopack is the default for `next dev` and `next build`.

```sh
npm install uql-orm pg server-only
```

## Where the pool lives

```ts title="src/db/uql.ts"
import 'server-only';
import { PgQuerierPool } from 'uql-orm/postgres';
import './entities'; // importing the module registers the decorated entities

// the dev server re-evaluates this on every hot reload; a fresh pool per reload leaks connections
declare global {
  var uqlPool: PgQuerierPool | undefined;
}

export const pool = (globalThis.uqlPool ??= new PgQuerierPool({
  connectionString: process.env.DATABASE_URL,
}));
```

`server-only` turns an accidental import from a client component into a build error rather than a bundled connection string. Server components and actions use the exported `pool`.

UQL’s decorators are the standard TC39 ones, so there are no decorator flags to add and nothing for Turbopack to trip over: field types are stated explicitly (`@Field({ type: String })`) rather than reflected. Next’s generated `tsconfig.json` needs no changes either: it already ships `moduleResolution: bundler` and `esnext` in `lib`, which is everything [Requirements](https://uql-orm.dev/getting-started.md) asks for.

## Server components

```tsx title="app/users/page.tsx"
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 },
        $where: { published: true },
        $limit: 5,
      },
    },
    $sort: { createdAt: 'desc' },
    $limit: 20,
  });

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          {user.name} ({user.posts.length})
        </li>
      ))}
    </ul>
  );
}
```

`users` is typed `User[]`, each with a typed `posts`.

## Route handlers

For callers outside your React tree: a mobile client, a webhook, a third party. Keep the default Node.js runtime, since `pg` needs TCP.

```ts title="app/api/users/route.ts"
import { NextResponse } from 'next/server';
import { pool } from '@/db/uql';
import { User } from '@/db/entities';

export async function GET() {
  const users = await pool.findMany(User, {
    $select: { id: true, name: true },
    $limit: 20,
  });
  return NextResponse.json(users);
}
```

## Server actions

```ts title="app/users/actions.ts"
'use server';

import { revalidatePath } from 'next/cache';
import { pool } from '@/db/uql';
import { Post, User } from '@/db/entities';
import { z } from 'zod';

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');
}
```

An action is a public endpoint with a generated URL, so build the query from validated input and never hand a raw `Query<User>` to the pool. Writes that must land together go in [`pool.transaction`](https://uql-orm.dev/querying/transactions.md).

## Auto-generated CRUD

```ts title="app/api/uql/[[...uql]]/route.ts"
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,
};
```

Next.js does not strip the prefix, hence `basePath`. Every entity now has typed REST endpoints (`/api/uql/user`, …) for [`HttpQuerier`](https://uql-orm.dev/browser.md). Route handler exports are named after the standard verbs, so the [`QUERY` transport](https://uql-orm.dev/http.md#http-query-rfc-10008) is not available here; keep the client on `GET`.

## Multi-tenancy

```ts title="src/db/withTenant.ts"
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(); // verified cookie or JWT, never a client-supplied id
  return session
    ? withContext({ tenantId: session.tenantId, userId: session.userId }, run)
    : run();
}
```

```tsx
const invoices = await withTenant(() => pool.findMany(Invoice, { $limit: 50 }));
```

`withContext` propagates across every `await` inside the callback, so a `security` [filter](https://uql-orm.dev/querying/filters.md) scopes each query, relations and cascades included, and fails closed without a context. Middleware cannot do this: it runs before the request and returns, so the store is gone by the time anything queries. The CRUD route gets the same treatment from `createFetchHandler`’s `getContext`. See [Multi-tenancy](https://uql-orm.dev/multi-tenancy.md).

> **Vercel**
>
> On Fluid compute, register the inner `pg` pool so idle connections close before the function suspends: `attachDatabasePool(pool.pool)` from `@vercel/functions`. See [Serverless](https://uql-orm.dev/serverless.md).
