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.
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.
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.
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.
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.
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
.
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.
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
})