---
title: ".procedure()"
description: "Register RPC procedures with .procedure(), mount raw routes with .route(), extract router types, and expose OpenAPI and REST."
---

> Documentation Index
> Fetch the complete documentation index at: https://outer.now/llms.txt
> Use this file to discover all available pages before exploring further.

# .procedure()

[`.resource()`](/outer/resource) cover CRUD. Everything else — a search endpoint, a webhook, a job trigger — is a procedure or a raw route.

## Register a procedure

`.procedure(name, cb, options?)` registers an oRPC procedure. `name` supports dot notation: `"user.me"` nests the procedure as `{ user: { me: proc } }` and serves it at `POST /rpc/user/me`. Procedures under the same namespace are deep-merged.

```ts
.procedure("user.me",     (base) => base.handler(...))
.procedure("user.update", (base) => base.input(z.object({...})).handler(...))
```

The callback receives the oRPC base builder, so chain `.input()`, `.output()`, and `.handler()` on it. Handlers get the [request context](/outer/database#request-context), including `context.db` and the resolved session.

Outer infers each procedure's input and output types into the router, so `client.user.me(...)` is fully typed. See [Extract the router type](#extract-the-router-type).

Guard a procedure with the optional third argument, `{ permission, roles? }` — see [Permissions](/outer/permissions#on-procedures).

Registering the same dot-path twice throws immediately. That covers two `.procedure()` calls and a name colliding with a resource-generated action, so a second registration never silently overwrites the first.

## Enrich context with middleware

`.middleware(mw)` adds an oRPC middleware to the base context. Whatever it returns via `next({ context })` is merged into `context` — and into the types — for every procedure defined after it.

```ts
.middleware(async ({ next }) => next({ context: { kv: useStorage() } }))
.procedure("cache.get", (base) =>
  base.input(z.object({ key: z.string() })).handler(async ({ context, input }) => {
return context.kv.getItem(input.key);
  }),
)
```

Call `.middleware()` before the procedures that need the extra fields. Auth stays on [`.auth()`](/outer/auth#read-the-session) — do not re-resolve the session in middleware.

## Throw errors

Import `ORPCError` from `@outerjs/server` and throw it for expected failures. Clients see a typed error; [`onError`](/outer/server#unexpected-errors) is **not** called.

```ts
import { ORPCError } from "@outerjs/server";

.procedure("post.publish", (base) =>
  base.input(z.object({ id: z.number() })).handler(async ({ context, input }) => {
const post = await context.db.query.post.findFirst({ where: { id: input.id } });
if (!post) throw new ORPCError("NOT_FOUND", { message: "Post not found" });
if (post.authorId !== context.user?.id) {
  throw new ORPCError("FORBIDDEN", { message: "Not your post" });
}
// ...
  }),
)
```

Common codes: `BAD_REQUEST` (400), `UNAUTHORIZED` (401), `FORBIDDEN` (403), `NOT_FOUND` (404), `CONFLICT` (409). Resource-generated CRUD maps Postgres constraint violations the same way — see [Resources → Errors](/outer/resource#errors).

## Mount a raw route

`.route(method, path, handler)` mounts a raw H3 route for webhooks, custom REST endpoints, or anything that does not fit the oRPC shape.

```ts
.route("post", "/webhooks/stripe", async (event, { db }) => {
  const body = await event.req.json();
  // ...
  return new Response("ok");
})
```

`handler` receives the H3 `event` and the same context procedures get, including the already-resolved session, so raw routes authorize the same way. Raw routes are registered before `/rpc/**` and win on overlapping paths.

## Extract the router type

Both the `Outer` instance and `BuiltOuter` expose a `router` property carrying the internal oRPC router type. Extract it with the exported `InferRouter<T>` helper:

```ts
// src/index.ts
export const outer = new Outer(...)
  .schema(v1_0)
  .procedure("user.me", (base) => base.handler(...))
  .build();
```

```ts
// outer.types.ts
import type { RouterClient } from "@orpc/server";
import type { InferRouter } from "@outerjs/server";
import type { outer } from "./src/index.js";

export type Router = InferRouter<typeof outer>;
export type AppClient = RouterClient<Router>;
```

Outer's core has no CLI. Write that file by hand, or generate it with your own script.

## Serve OpenAPI and REST

`.openapi(config?)` mounts `GET /openapi.json` **and** a plain-JSON REST surface at `/rest/**`. Neither is mounted unless you call it, and calling it with no arguments enables both. It can appear anywhere before `.build()`. Requires the optional peers `@orpc/openapi` and `@orpc/zod`.

```ts
.openapi() // always enabled
.openapi({ enabled: import.meta.env.DEV }) // dev and staging only
```

| Option    | Type      | Default | Description                                     |
| --------- | --------- | ------- | ----------------------------------------------- |
| `enabled` | `boolean` | `true`  | Whether to mount `/openapi.json` and `/rest/**` |

The `/rpc/**` handler speaks oRPC's own wire protocol, which generic OpenAPI clients cannot use. So when `.openapi()` is enabled, the same router is also served through oRPC's `OpenAPIHandler` under `/rest/**`, matching the generated spec exactly.

| Surface      | Path                   | Audience                                  |
| ------------ | ---------------------- | ----------------------------------------- |
| Typed RPC    | `POST /rpc/<dot/path>` | [`@outerjs/sdk`](/getting-started/client) |
| OpenAPI JSON | `GET /openapi.json`    | Spec consumers, Hub, codegen              |
| Plain REST   | `/rest/**`             | Curl, Postman, non-oRPC clients           |

Call REST with plain JSON — for example `POST /rest/post/create` with `{"title": "..."}`. Dot-separated procedure names become path segments under `/rest`. The spec advertises `<baseUrl>/rest` as its server URL.

The generated document is OpenAPI 3.x, from `@orpc/openapi`. Its title comes from the `name` param and its version from the last registered schema. Procedures with `.input(zodSchema)` and `.output(zodSchema)` are fully documented. Output schemas are not inferred from handler return types, so add an explicit `.output()` when you want responses documented.

## Next steps

- [Client](/getting-started/client) — call these procedures from the browser or SSR
- [Database](/outer/database) — query inside your handlers
- [Permissions](/outer/permissions) — guard what you just registered
- [`.mcp()`](/outer/mcp) — expose procedures as agent tools

Source: https://outer.now/outer/procedure/index.mdx
