Middleware
Apply middleware to procedures to add cross-cutting concerns like authentication, logging, or rate limiting.
Procedure-Level Middleware
Apply middleware to specific procedures using .use():
import { router, publicProcedure, ok } from "@alt-stack/server-hono";
import { z } from "zod";
export const userRouter = router({
create: publicProcedure
.input({
body: z.object({
name: z.string(),
email: z.string().email(),
}),
})
.output(
z.object({
id: z.string(),
})
)
.use(async (opts) => {
// Log before handler
const { ctx, next } = opts;
console.log("Creating user:", ctx.input.name);
return next();
})
.post((opts) => {
return ok({ id: "1" });
}),
});
Context Extension
Middleware can extend the context by passing updated context to next(). This follows the tRPC pattern:
import { router, publicProcedure, init, ok, err, TaggedError } from "@alt-stack/server-hono";
import { z } from "zod";
interface AppContext {
user: { id: string; name: string } | null;
}
class UnauthorizedError extends TaggedError {
readonly _tag = "UnauthorizedError" as const;
constructor(public readonly message: string = "Authentication required") {
super(message);
}
}
const UnauthorizedErrorSchema = z.object({
_tag: z.literal("UnauthorizedError"),
message: z.string(),
});
const factory = init<AppContext>();
const loggerMiddleware = async (opts: {
ctx: any;
next: (opts?: { ctx: Partial<any> }) => Promise<any>;
}) => {
const { ctx, next } = opts;
const start = Date.now();
const result = await next();
const duration = Date.now() - start;
console.log(`Request took ${duration}ms`);
return result;
};
const authMiddleware = async (opts: {
ctx: any;
next: (opts?: { ctx: Partial<any> }) => Promise<any>;
}) => {
const { ctx, next } = opts;
const user = await authenticate(ctx.hono.req);
if (!user) {
return err(new UnauthorizedError());
}
// Extend context - user is now non-null in subsequent handlers
return next({ ctx: { user } });
};
const protectedProcedure = factory.procedure
.errors({ 401: UnauthorizedErrorSchema })
.use(loggerMiddleware)
.use(authMiddleware);
export const appRouter = router({
profile: protectedProcedure
.input({})
.get((opts) => {
// opts.ctx.user is guaranteed to be non-null
const { ctx } = opts;
return ok({ id: ctx.user!.id, name: ctx.user!.name });
}),
});
Multiple Middleware
Chain multiple middleware on the same procedure:
import { router, publicProcedure, init, ok, err, TaggedError } from "@alt-stack/server-hono";
import { z } from "zod";
interface AppContext {
user: { id: string; role: string } | null;
}
class ForbiddenError extends TaggedError {
readonly _tag = "ForbiddenError" as const;
constructor(public readonly message: string = "Access denied") {
super(message);
}
}
const ForbiddenErrorSchema = z.object({
_tag: z.literal("ForbiddenError"),
message: z.string(),
});
const factory = init<AppContext>();
const loggerMiddleware = async (opts: any) => {
console.log("Request started");
return opts.next();
};
const authMiddleware = async (opts: any) => {
const user = await getUser(opts.ctx);
return opts.next({ ctx: { user } });
};
const adminMiddleware = async (opts: any) => {
if (opts.ctx.user?.role !== "admin") {
return err(new ForbiddenError("Admin access required"));
}
return opts.next();
};
const adminProcedure = factory.procedure
.errors({ 403: ForbiddenErrorSchema })
.use(loggerMiddleware)
.use(authMiddleware)
.use(adminMiddleware);
export const adminRouter = router({
settings: adminProcedure.get(() => {
return ok({ admin: true });
}),
});
Middleware executes in the order they're defined.
Reusable Procedures
Create reusable procedures with middleware to reuse authentication or other middleware across multiple routes. See the Reusable Procedures guide for details:
import { router, publicProcedure, init, ok, err, TaggedError } from "@alt-stack/server-hono";
import { z } from "zod";
interface AppContext {
user: { id: string; name: string } | null;
}
class UnauthorizedError extends TaggedError {
readonly _tag = "UnauthorizedError" as const;
constructor(public readonly message: string = "Authentication required") {
super(message);
}
}
const UnauthorizedErrorSchema = z.object({
_tag: z.literal("UnauthorizedError"),
message: z.string(),
});
const factory = init<AppContext>();
// Create reusable procedures
const publicProc = publicProcedure;
const protectedProcedure = factory.procedure
.errors({ 401: UnauthorizedErrorSchema })
.use(async (opts) => {
// Auth middleware
const { ctx, next } = opts;
if (!ctx.user) {
return err(new UnauthorizedError());
}
return next({ ctx: { user: ctx.user } });
});
// Use procedures
export const appRouter = router({
hello: publicProc.get(() => ok("hello")),
profile: protectedProcedure
.input({})
.output(
z.object({
id: z.string(),
name: z.string(),
})
)
.get((opts) => {
return ok(opts.ctx.user!);
}),
});
Middleware Chaining and Context Flow
Middleware can chain together, with each middleware able to extend the context:
import { router, publicProcedure, init, ok } from "@alt-stack/server-hono";
interface AppContext {
user: { id: string; role: string } | null;
requestId: string;
isAdmin: boolean;
}
const factory = init<AppContext>();
// First middleware adds requestId
const requestIdMiddleware = async (opts: any) => {
const requestId = crypto.randomUUID();
return opts.next({ ctx: { requestId } });
};
// Second middleware adds user
const authMiddleware = async (opts: any) => {
const user = await getUser(opts.ctx);
return opts.next({ ctx: { user } });
};
// Third middleware adds isAdmin based on user role
const adminCheckMiddleware = async (opts: any) => {
const isAdmin = opts.ctx.user?.role === "admin";
return opts.next({ ctx: { isAdmin } });
};
const adminProcedure = factory.procedure
.use(requestIdMiddleware)
.use(authMiddleware)
.use(adminCheckMiddleware);
export const adminRouter = router({
dashboard: adminProcedure
.input({})
.get((opts) => {
// All context extensions are available
const { ctx } = opts;
return ok({
requestId: ctx.requestId,
userId: ctx.user!.id,
isAdmin: ctx.isAdmin,
});
}),
});