> 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

# React Router

> Use UQL in React Router framework mode loaders, actions, resource routes, and middleware.

Source: https://uql-orm.dev/react-router

Framework mode (what Remix became) is fetch-native throughout: loaders and actions receive a web `Request` and may return a `Response`, and middleware wraps the request. No adapter anywhere. Written for v8; v7 differs only in that middleware was still behind a future flag there.

## Where the pool lives

The `.server.ts` suffix is a hard boundary: the Vite plugin strips those modules from the client bundle, so importing one from a component is a build error rather than a leaked connection string.

```ts title="app/db.server.ts"
import { PgQuerierPool } from 'uql-orm/postgres';
import './models';

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

Loaders and actions use the exported `pool`.

## Loaders and actions

```tsx title="app/routes/posts.tsx"
import { pool } from '~/db.server';
import { Post } from '~/models';
import type { Route } from './+types/posts';

export async function loader({ request }: Route.LoaderArgs) {
  const skip = Number(new URL(request.url).searchParams.get('skip')) || 0;
  return {
    posts: await pool.findMany(Post, {
      $select: { id: true, title: true },
      $populate: { author: { $select: { name: true } } },
      $where: { published: true },
      $sort: { createdAt: 'desc' },
      $limit: 20,
      $skip: skip,
    }),
  };
}

export default function Posts({ loaderData }: Route.ComponentProps) {
  return (
    <ul>
      {loaderData.posts.map((post) => (
        <li key={post.id}>
          {post.title} - {post.author.name}
        </li>
      ))}
    </ul>
  );
}
```

`loaderData` is typed from the loader’s return type, each post with a typed `author`. Rows are plain objects, so there is nothing to map for the single-fetch serializer.

```ts title="app/routes/posts.new.tsx"
import { redirect } from 'react-router';
import { pool } from '~/db.server';
import { Post } from '~/models';
import type { Route } from './+types/posts.new';
import { z } from 'zod';

const NewPost = z.object({ title: z.string().trim().min(1) });

export async function action({ request }: Route.ActionArgs) {
  const form = NewPost.safeParse(Object.fromEntries(await request.formData()));
  if (!form.success) {
    return { errors: z.flattenError(form.error).fieldErrors };
  }
  return redirect(`/posts/${await pool.insertOne(Post, form.data)}`);
}
```

An action is a public endpoint: build the query from validated input, never from a raw `Query<Post>`. Several writes go in [`pool.transaction`](https://uql-orm.dev/querying/transactions.md).

## Auto-generated CRUD

A route module with no default export is a resource route. Point a splat at one:

```ts title="app/routes.ts"
import { type RouteConfig, index, route } from '@react-router/dev/routes';

export default [
  index('routes/home.tsx'),
  route('api/uql/*', 'routes/uql.ts'),
] satisfies RouteConfig;
```

```ts title="app/routes/uql.ts"
import { createFetchHandler } from 'uql-orm/http';
import { pool } from '~/db.server';
import { Post, User } from '~/models';
import type { Route } from './+types/uql';

const handler = createFetchHandler({
  pool,
  include: [Post, User],
  basePath: '/api/uql',
});

export const loader = ({ request }: Route.LoaderArgs) => handler(request);
export const action = ({ request }: Route.ActionArgs) => handler(request);
```

React Router does not strip the prefix, hence `basePath`. `loader` serves `GET` and `HEAD`, `action` serves the write verbs, which is the whole [wire protocol](https://uql-orm.dev/http.md#wire-protocol) minus the [`QUERY` transport](https://uql-orm.dev/http.md#http-query-rfc-10008): that split is keyed to named verbs, so keep the [browser client](https://uql-orm.dev/browser.md) on `GET`.

## Multi-tenancy

Middleware wraps `next()`, so `withContext` drops straight in. On the root route it covers every loader, action and resource route below:

```ts title="app/root.tsx"
import { withContext } from 'uql-orm';
import type { Route } from './+types/root';

export const middleware: Route.MiddlewareFunction[] = [
  async ({ request }, next) => {
    const session = await getSession(request.headers.get('cookie'));
    return session
      ? withContext(
          { tenantId: session.tenantId, userId: session.userId },
          () => next(),
        )
      : next();
  },
];
```

A `security` [filter](https://uql-orm.dev/querying/filters.md) then scopes every query, relations and cascades included, and fails closed without a context. See [Multi-tenancy](https://uql-orm.dev/multi-tenancy.md).

Data mode and SPA mode have no server in the router: mount the [HTTP core](https://uql-orm.dev/http.md) on whatever hosts your API and talk to it with [`HttpQuerier`](https://uql-orm.dev/browser.md) or the [TanStack Query recipe](https://uql-orm.dev/react-query.md).
