Skip to content

Middleware in oRPC

Middleware is a powerful feature in oRPC that enables reusable and extensible procedures. It allows you to:

  • Intercept, hook into, or listen to a handler's execution.
  • Inject or guard the execution context.

Overview

Middleware is a function that takes a next function as a parameter and either returns the result of next or modifies the result before returning it.

ts
const 
authMiddleware
=
os
.
$context
<{
something
?: string }>() // <-- define dependent-context
.
middleware
(async ({
context
,
next
}) => {
// Execute logic before the handler const
result
= await
next
({
context
: { // Pass additional context
user
: {
id
: 1,
name
: 'John' }
} }) // Execute logic after the handler return
result
}) const
example
=
os
.
use
(
authMiddleware
)
.
handler
(async ({
context
}) => {
const
user
=
context
.
user
})

Dependent context

Before .middleware, you can .$context to specify the dependent context, which must be satisfied when the middleware is used.

Inline Middleware

Middleware can be defined inline within .use, which is useful for simple middleware functions.

ts
const example = os
  .use(async ({ context, next }) => {
    // Execute logic before the handler
    return next()
  })
  .handler(async ({ context }) => {
    // Handler logic
  })

Middleware Context

Middleware can use to inject or guard the context.

ts
const 
setting
=
os
.
use
(async ({
context
,
next
}) => {
return
next
({
context
: {
auth
: await
auth
() // <-- inject auth payload
} }) }) .
use
(async ({
context
,
next
}) => {
if (!
context
.
auth
) { // <-- guard auth
throw new
ORPCError
('UNAUTHORIZED')
} return
next
({
context
: {
auth
:
context
.
auth
// <-- override auth
} }) }) .
handler
(async ({
context
}) => {
console
.
log
(
context
.
auth
) // <-- access auth
})

When you pass additional context to next, it will be merged with the existing context.

Middleware Input

Middleware can access input, enabling use cases like permission checks.

ts
const canUpdate = os.middleware(async ({ context, next }, input: number) => {
  // Perform permission check
  return next()
})

const ping = os
  .input(z.number())
  .use(canUpdate)
  .handler(async ({ input }) => {
    // Handler logic
  })

// Mapping input if necessary
const pong = os
  .input(z.object({ id: z.number() }))
  .use(canUpdate, input => input.id)
  .handler(async ({ input }) => {
    // Handler logic
  })

Middleware Output

Middleware can also modify the output of a handler, such as implementing caching mechanisms.

ts
const cacheMid = os.middleware(async ({ context, next, path }, input, output) => {
  const cacheKey = path.join('/') + JSON.stringify(input)

  if (db.has(cacheKey)) {
    return output(db.get(cacheKey))
  }

  const result = await next({})

  db.set(cacheKey, result.output)

  return result
})

Concatenation

Multiple middleware functions can be combined using .concat.

ts
const concatMiddleware = aMiddleware
  .concat(os.middleware(async ({ next }) => next()))
  .concat(anotherMiddleware)

Built-in Middlewares

oRPC provides some built-in middlewares that can be used to simplify common use cases.

ts
import { onError, onFinish, onStart, onSuccess } from '@orpc/server'

const ping = os
  .use(onStart(() => {
    // Execute logic before the handler
  }))
  .use(onSuccess(() => {
    // Execute when the handler succeeds
  }))
  .use(onError(() => {
    // Execute when the handler fails
  }))
  .use(onFinish(() => {
    // Execute logic after the handler
  }))
  .handler(async ({ context }) => {
    // Handler logic
  })

Released under the MIT License.