From a0a8197e49c321e14fb47b040355929af11ebb1b Mon Sep 17 00:00:00 2001 From: fernando yaeda Date: Wed, 17 Dec 2025 16:54:49 -0300 Subject: [PATCH 1/4] docs: add type-safe errors in middleware example --- .../contract-first/src/contract/auth.ts | 5 ++++ .../contract-first/src/contract/planet.ts | 4 +++ .../contract-first/src/middlewares/auth.ts | 30 +++++++++++++++++++ playgrounds/contract-first/src/orpc.ts | 23 +++----------- .../contract-first/src/routers/auth.ts | 2 +- 5 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 playgrounds/contract-first/src/middlewares/auth.ts diff --git a/playgrounds/contract-first/src/contract/auth.ts b/playgrounds/contract-first/src/contract/auth.ts index 88185601e..02ca21ddb 100644 --- a/playgrounds/contract-first/src/contract/auth.ts +++ b/playgrounds/contract-first/src/contract/auth.ts @@ -29,4 +29,9 @@ export const me = oc summary: 'Get the current user', tags: ['Authentication'], }) + .errors({ + UNAUTHORIZED: { + message: 'User is not authenticated', + }, + }) .output(UserSchema) diff --git a/playgrounds/contract-first/src/contract/planet.ts b/playgrounds/contract-first/src/contract/planet.ts index 0e7be43fc..22a40bcbf 100644 --- a/playgrounds/contract-first/src/contract/planet.ts +++ b/playgrounds/contract-first/src/contract/planet.ts @@ -24,6 +24,9 @@ export const createPlanet = oc summary: 'Create a planet', tags: ['Planets'], }) + .errors({ + UNAUTHORIZED: { message: 'User is not authenticated' }, + }) .input(NewPlanetSchema) .output(PlanetSchema) @@ -49,6 +52,7 @@ export const updatePlanet = oc tags: ['Planets'], }) .errors({ + UNAUTHORIZED: { message: 'User is not authenticated' }, NOT_FOUND: { message: 'Planet not found', data: z.object({ id: UpdatePlanetSchema.shape.id }), diff --git a/playgrounds/contract-first/src/middlewares/auth.ts b/playgrounds/contract-first/src/middlewares/auth.ts new file mode 100644 index 000000000..7613e679f --- /dev/null +++ b/playgrounds/contract-first/src/middlewares/auth.ts @@ -0,0 +1,30 @@ +import { implement } from '@orpc/server' +import { contract } from '../contract' +import type { UserSchema } from '../schemas/user' +import type { z } from 'zod' + +export interface AuthContext { + user?: z.infer +} + +export const authMiddleware = implement(contract) + .$context() + .middleware(({ context, next, errors }) => { + /** + * Type narrowing check for error constructor access. + * Required when not all procedures in the contract define this error. + */ + if (!('UNAUTHORIZED' in errors)) { + throw new Error('Contract is missing UNAUTHORIZED error') + } + + if (!context.user) { + throw errors.UNAUTHORIZED() + } + + return next({ + context: { + user: context.user, + }, + }) + }) diff --git a/playgrounds/contract-first/src/orpc.ts b/playgrounds/contract-first/src/orpc.ts index 40ecd5d88..4783cf22f 100644 --- a/playgrounds/contract-first/src/orpc.ts +++ b/playgrounds/contract-first/src/orpc.ts @@ -1,25 +1,10 @@ -import type { z } from 'zod' -import type { UserSchema } from './schemas/user' -import { implement, ORPCError } from '@orpc/server' +import { implement } from '@orpc/server' import { dbProviderMiddleware } from './middlewares/db' import { contract } from './contract' - -export interface ORPCContext { - user?: z.infer -} +import { authMiddleware } from './middlewares/auth' export const pub = implement(contract) - .$context() .use(dbProviderMiddleware) -export const authed = pub.use(({ context, next }) => { - if (!context.user) { - throw new ORPCError('UNAUTHORIZED') - } - - return next({ - context: { - user: context.user, - }, - }) -}) +export const authed = pub + .use(authMiddleware) diff --git a/playgrounds/contract-first/src/routers/auth.ts b/playgrounds/contract-first/src/routers/auth.ts index 19c4c66ae..e604d61a3 100644 --- a/playgrounds/contract-first/src/routers/auth.ts +++ b/playgrounds/contract-first/src/routers/auth.ts @@ -15,6 +15,6 @@ export const signin = pub.auth.signin }) export const me = authed.auth.me - .handler(async ({ input, context }) => { + .handler(async ({ context }) => { return context.user }) From 2fcee1104d80477bd55edfc4a58fb7e6d9e1dcc8 Mon Sep 17 00:00:00 2001 From: fernando yaeda Date: Fri, 19 Dec 2025 12:00:30 -0300 Subject: [PATCH 2/4] docs: add Protected Procedures documentation with type-safe error handling --- apps/content/.vitepress/config.ts | 1 + .../contract-first/protected-procedures.md | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 apps/content/docs/contract-first/protected-procedures.md diff --git a/apps/content/.vitepress/config.ts b/apps/content/.vitepress/config.ts index c174dc910..ed9969384 100644 --- a/apps/content/.vitepress/config.ts +++ b/apps/content/.vitepress/config.ts @@ -107,6 +107,7 @@ export default withMermaid(defineConfig({ { text: 'Define Contract', link: '/docs/contract-first/define-contract' }, { text: 'Implement Contract', link: '/docs/contract-first/implement-contract' }, { text: 'Router to Contract', link: '/docs/contract-first/router-to-contract' }, + { text: 'Protected Procedures', link: '/docs/contract-first/protected-procedures' }, ], }, { diff --git a/apps/content/docs/contract-first/protected-procedures.md b/apps/content/docs/contract-first/protected-procedures.md new file mode 100644 index 000000000..6f6c936cd --- /dev/null +++ b/apps/content/docs/contract-first/protected-procedures.md @@ -0,0 +1,96 @@ +--- +title: Protected Procedures +description: Learn how to create protected procedures with authentication middleware +--- + +# Protected Procedures + +You can build protected procedures by defining [errors](/docs/error-handling) in your contract, chaining authentication [middleware](/docs/middleware) on top of a implementer, and then using it in your procedures. + +## Define Error in Contracts + +Define errors in your contracts using the `.errors()` method. This allows the client to know exactly what errors a procedure can throw. + +```ts twoslash +import { oc } from '@orpc/contract' +import * as z from 'zod' +// ---cut--- +export const contract = oc + .errors({ + UNAUTHORIZED: { + message: 'User is not authenticated', + }, + }) + .input( + z.object({ + id: z.number() + }) + ) + .output( + z.object({ + id: z.number(), + name: z.string() + }) + ) +``` + +## Type-Safe Errors in Middleware + +Middlewares can also throw type-safe errors. However, once not every procedure defined in your contract contains the error used in the middleware, you must use **type narrowing** to make the error accessible from the `errors` object. + +```ts +export const authMiddleware = implement(contract) + .$context<{ user?: { id: string, email: string } }>() + .middleware(({ context, next, errors }) => { + // Type narrowing: Check if UNAUTHORIZED error is defined in the contract + if (!('UNAUTHORIZED' in errors)) { + throw new Error('Contract is missing UNAUTHORIZED error') + } + + if (!context.user) { + // Throw type-safe error + throw errors.UNAUTHORIZED() + } + + return next({ + context: { + user: context.user, + }, + }) + }) +``` + +## Setting Up Protected Procedures + +Start by creating a public implementer with your contract. Then extend it with an authentication middleware to create the protected one: + +```ts twoslash +export const pub = implement(contract) + .use(dbProviderMiddleware) + +export const authed = pub + .use(authMiddleware) +``` + +:::info +By using `pub.use(authMiddleware)`, the protected implementer inherits all middleware from the public implementer and adds authentication on top. +::: + +## Protect the Procedure + +Now use `pub` for public procedures and `authed` for protected ones in your router: + +```ts +// Public procedure - no authentication required +export const listPlanets = pub.planet.list + .handler(async ({ input, context }) => { + return context.db.planets.list(input.limit, input.cursor) + }) + +// Protected procedure - requires authentication +export const createPlanet = authed.planet.create + .handler(async ({ input, context }) => { + // context.user is guaranteed to exist here due to authMiddleware + return context.db.planets.create(input, context.user) + }) +``` From 47acaa1019f786019ef6151e956b0c1de72a1eba Mon Sep 17 00:00:00 2001 From: fernando yaeda Date: Fri, 19 Dec 2025 12:09:46 -0300 Subject: [PATCH 3/4] fix(docs): fix grammar and improve sentence clarity Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/content/docs/contract-first/protected-procedures.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/content/docs/contract-first/protected-procedures.md b/apps/content/docs/contract-first/protected-procedures.md index 6f6c936cd..996e583b2 100644 --- a/apps/content/docs/contract-first/protected-procedures.md +++ b/apps/content/docs/contract-first/protected-procedures.md @@ -5,7 +5,7 @@ description: Learn how to create protected procedures with authentication middle # Protected Procedures -You can build protected procedures by defining [errors](/docs/error-handling) in your contract, chaining authentication [middleware](/docs/middleware) on top of a implementer, and then using it in your procedures. +You can build protected procedures by defining [errors](/docs/error-handling) in your contract, chaining authentication [middleware](/docs/middleware) on top of an implementer, and then using it in your procedures. ## Define Error in Contracts @@ -36,7 +36,7 @@ export const contract = oc ## Type-Safe Errors in Middleware -Middlewares can also throw type-safe errors. However, once not every procedure defined in your contract contains the error used in the middleware, you must use **type narrowing** to make the error accessible from the `errors` object. +Middlewares can also throw type-safe errors. However, since not every procedure defined in your contract may contain the error used in the middleware, you must use **type narrowing** to make the error accessible from the `errors` object. ```ts export const authMiddleware = implement(contract) From f4a0358a350ff903d2398488cd1c6afce6c9247f Mon Sep 17 00:00:00 2001 From: fernando yaeda Date: Fri, 19 Dec 2025 12:18:32 -0300 Subject: [PATCH 4/4] docs: fix code block formatting in Protected Procedures documentation --- apps/content/docs/contract-first/protected-procedures.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/content/docs/contract-first/protected-procedures.md b/apps/content/docs/contract-first/protected-procedures.md index 996e583b2..34d1c50c7 100644 --- a/apps/content/docs/contract-first/protected-procedures.md +++ b/apps/content/docs/contract-first/protected-procedures.md @@ -64,7 +64,7 @@ export const authMiddleware = implement(contract) Start by creating a public implementer with your contract. Then extend it with an authentication middleware to create the protected one: -```ts twoslash +```ts export const pub = implement(contract) .use(dbProviderMiddleware)