Middleware
Middleware in xRPC intercepts requests before they reach your handlers, allowing you to handle authentication, logging, request ID injection, and other cross-cutting concerns. Middleware can extend the request context with typed data.
Note: TypeScript middleware hooks are planned for the
ts-expresstarget. Go middleware is available today.
Is Middleware Required?
No. xRPC middleware is optional. Your frameworkâs native middleware works perfectly fine.
| Use framework middleware (Express, Gin, etc.) for: | Use xRPC middleware when you need: |
|---|---|
| CORS, compression, rate limiting | Typed context passed to handlers |
| Standard request logging | Planned: contract-defined context schema |
| Static file serving | Planned: cross-language type generation |
If you just need standard middleware features, use what your framework provides. Contract-defined context schemas and cross-language context generation are planned; today, middleware is defined as functions in your contract and handled per target.
Defining Middleware in Contract
Define middleware in your contract. Cross-language typed context is planned; today, each target handles context differently.
import { createRouter, type Middleware } from 'xrpckit';
type AuthContext = {
userId: string;
role: 'user' | 'admin';
};
const authMiddleware: Middleware<AuthContext> = async (req, ctx) => {
const token = req.headers.get('authorization');
return { ...ctx, userId: extractUserId(token), role: 'user' };
};
export const router = createRouter({
middleware: [authMiddleware],
// ... endpoints
});
TypeScript Implementation
Planned (ts-express): The API below is a draft and may change. It illustrates the intended flow.
import { createMiddleware, createHandler } from './xrpc/server';
// Middleware: validate token and add typed user data to context
export const authMiddleware = createMiddleware('auth', async (req, ctx) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) throw new UnauthorizedError('Missing token');
const payload = await verifyJWT(token);
return { ...ctx, userId: payload.sub, role: payload.role };
});
// Handler: receives fully typed context
export const getProfile = createHandler('user.getProfile', async (input, ctx) => {
// ctx.userId and ctx.role are typed from contract
if (ctx.role !== 'admin' && ctx.userId !== input.id) {
throw new Error('Not authorized');
}
return await db.users.findById(input.id);
});
Throwing an error in middleware short-circuits the request and returns an error response.
Go Implementation
Go middleware follows the same pattern using ctx.Data for context storage:
// Middleware: validate token and store user data in context
func authMiddleware(ctx *xrpc.Context) *xrpc.MiddlewareResult {
token := ctx.Request.Header.Get("Authorization")
if token == "" {
return xrpc.NewMiddlewareError(fmt.Errorf("unauthorized"))
}
userId, role, err := validateToken(strings.TrimPrefix(token, "Bearer "))
if err != nil {
return xrpc.NewMiddlewareError(err)
}
ctx.Data["userId"] = userId
ctx.Data["role"] = role
return xrpc.NewMiddlewareResult(ctx)
}
// Handler: access context data from the map
func getProfileHandler(ctx *xrpc.Context, input xrpc.GetProfileInput) (xrpc.GetProfileOutput, error) {
userId, _ := ctx.Data["userId"].(string)
role, _ := ctx.Data["role"].(string)
if role != "admin" && userId != input.Id {
return nil, fmt.Errorf("not authorized")
}
return db.FindUser(input.Id)
}
Return xrpc.NewMiddlewareError() to short-circuit with an error, or xrpc.NewMiddlewareResponse() for a custom HTTP response.
Context Helpers
xRPC context access by language:
| Language | Store context | Retrieve context |
|---|---|---|
| TypeScript | return { ...ctx, userId } |
ctx.userId (directly typed) |
| Go | ctx.Data["userId"] = value |
ctx.Data["userId"].(string) |
Go access uses type assertions to handle missing context gracefully. TypeScript context typing is planned for ts-express.
Middleware Order
Middleware executes in registration order. Recommended ordering:
- Request ID - Generate/extract ID first for tracing
- Logging - Log incoming requests with ID
- Authentication - Validate tokens, extract user info
- Authorization - Check permissions (or handle per-handler)
router.Use(requestIdMiddleware) // 1st
router.Use(loggingMiddleware) // 2nd
router.Use(authMiddleware) // 3rd
Next Steps
- TypeScript Server - Planned TypeScript server setup
- Go Server - Full Go server setup
- API Contract - Define your schema