Skip to content

Middleware

Sometimes you need to run logic before a procedure handler executes — logging the request, checking rate limits, transforming input, or injecting additional context. Middleware chains let you compose these concerns without cluttering your handler code.

Add middleware with .use():

import { procedure } from '@veloxts/velox';
const loggedProcedure = procedure()
.use(async ({ ctx, next }) => {
console.log('Request started');
const result = await next();
console.log('Request completed');
return result;
})
.query(handler);
type Middleware = (opts: {
ctx: Context;
input: unknown;
next: () => Promise<unknown>;
}) => Promise<unknown>;
const loggingMiddleware = async ({ ctx, next }) => {
const start = Date.now();
const result = await next();
const duration = Date.now() - start;
console.log(`${ctx.request.method} ${ctx.request.url} - ${duration}ms`);
return result;
};
const timingMiddleware = async ({ ctx, next }) => {
const start = process.hrtime.bigint();
const result = await next();
const end = process.hrtime.bigint();
ctx.reply.header('X-Response-Time', `${Number(end - start) / 1e6}ms`);
return result;
};
const errorMiddleware = async ({ next }) => {
try {
return await next();
} catch (error) {
// Log to error tracking service
console.error('Procedure error:', error);
throw error;
}
};
const tenantMiddleware = async ({ ctx, next }) => {
const tenantId = ctx.request.headers['x-tenant-id'];
ctx.tenant = await getTenant(tenantId);
return next();
};

Middleware runs in order:

const securedProcedure = procedure()
.use(loggingMiddleware) // 1. Log request
.use(authMiddleware) // 2. Verify auth
.use(rateLimitMiddleware) // 3. Check rate limit
.guard(authenticated) // 4. Require auth
.query(handler); // 5. Execute handler

Create middleware-enhanced base procedures:

// Base with logging
export const loggedProcedure = procedure().use(loggingMiddleware);
// Base with auth
export const authedProcedure = procedure()
.guard(authenticated)
.use(loggingMiddleware);
// Use in procedures
export const userProcedures = procedures('users', {
listUsers: loggedProcedure
.query(async ({ ctx }) => {
const users = await ctx.db.user.findMany();
return resourceCollection(users, UserSchema.authenticated);
}),
getProfile: authedProcedure
.query(({ ctx }) => {
return resource(ctx.user, UserSchema.authenticated);
}),
});
FeatureMiddlewareGuards
PurposeRequest processingAuthorization
Can modify contextYesNo
Can modify responseYesNo
Failure behaviorThrow or continueBlock request

Use middleware for cross-cutting concerns (logging, timing). Use guards for authorization (authenticated, hasRole).

.useAfter() is the counterpart to .use() — it runs after the handler succeeds:

procedure()
.mutation(async ({ input, ctx }) => {
return ctx.db.order.create({ data: input });
})
.useAfter(({ input, result, ctx }) => {
console.log(`Order ${result.id} created by ${ctx.user.id}`);
})

Hooks run after events are emitted (if any). Errors are caught and logged — they never fail the response. The return value is ignored: hooks cannot modify the result. Multiple .useAfter() calls chain in registration order.

See Business Logic for the full picture including pipelines and events.