Reusable Procedures
Create reusable procedures with middleware to follow the tRPC pattern. This allows you to define common authentication, validation, or other middleware once and reuse it across multiple routes.
Basic Pattern
Use publicProcedure or init() to create procedures:
import { router, publicProcedure, init } from "@alt-stack/server-hono";
import { z } from "zod";
interface AppContext {
user: { id: string; name: string } | null;
}
const factory = init<AppContext>();
// Create reusable procedures
const publicProc = publicProcedure;
const protectedProcedure = factory.procedure
.errors({
401: z.object({
error: z.object({
code: z.literal("UNAUTHORIZED"),
message: z.string(),
}),
}),
})
.use(async function isAuthed(opts) {
const { ctx, next } = opts;
// `ctx.user` is nullable
if (!ctx.user) {
throw ctx.error({
error: {
code: "UNAUTHORIZED" as const,
message: "Authentication required",
},
});
}
// ✅ Pass updated context where user is non-null
return next({
ctx: {
user: ctx.user, // ✅ user value is known to be non-null now
},
});
});
// Create a router using the new tRPC-style API
export const appRouter = router({
hello: publicProc.get(() => {
return "hello world";
}),
profile: protectedProcedure
.input({})
.output(
z.object({
id: z.string(),
name: z.string(),
})
)
.get((opts) => {
// opts.ctx.user is guaranteed to be non-null after middleware
const { ctx } = opts;
return {
id: ctx.user!.id,
name: ctx.user!.name,
};
}),
secret: protectedProcedure
.input({})
.output(
z.object({
secret: z.string(),
})
)
.get(() => {
return { secret: "sauce" };
}),
});
Configuring Procedures
Procedures support the same configuration methods as regular routes:
Setting Default Input
import { router, publicProcedure } from "@alt-stack/server-hono";
import { z } from "zod";
const validatedProcedure = publicProcedure.input({
query: z.object({
apiKey: z.string().min(1),
}),
});
// All routes using this procedure will require apiKey in query
export const dataRouter = router({
list: validatedProcedure
.input({
body: z.object({ filter: z.string() }), // Additional input
})
.get((opts) => {
// opts.input.apiKey is available (from procedure)
// opts.input.filter is available (from route)
const { input } = opts;
return [];
}),
});
Setting Default Output
import { router, publicProcedure } from "@alt-stack/server-hono";
import { z } from "zod";
const jsonProcedure = publicProcedure.output(
z.object({
success: z.boolean(),
})
);
export const actionRouter = router({
create: jsonProcedure
.input({
body: z.object({ action: z.string() }),
})
.post((opts) => {
// output is automatically set from procedure
return { success: true };
}),
});
Setting Default Errors
import { router, publicProcedure } from "@alt-stack/server-hono";
import { z } from "zod";
const errorProcedure = publicProcedure.errors({
401: z.object({
error: z.object({
code: z.literal("UNAUTHORIZED"),
message: z.string(),
}),
}),
});
export const protectedRouter = router({
data: errorProcedure
.input({})
.output(z.string())
.get((opts) => {
const { ctx } = opts;
if (someCondition) {
throw ctx.error({
error: {
code: "UNAUTHORIZED",
message: "Not authorized",
},
});
}
return "success";
}),
});
Combining Procedure and Route Errors
Errors defined on procedures are automatically merged with errors defined on routes. Route errors take precedence when the same status code is defined in both:
import { router, publicProcedure } from "@alt-stack/server-hono";
import { z } from "zod";
// Procedure defines common authentication error
const apiProcedure = publicProcedure.errors({
401: z.object({
error: z.object({
code: z.literal("UNAUTHORIZED"),
message: z.string(),
}),
}),
});
export const userRouter = router({
"{id}": apiProcedure
.input({
params: z.object({ id: z.string() }),
})
.output(z.object({ id: z.string(), name: z.string() }))
.errors({
// 401 is inherited from procedure
// Add additional route-specific errors
404: z.object({
error: z.object({
code: z.literal("NOT_FOUND"),
message: z.string(),
}),
}),
403: z.object({
error: z.object({
code: z.literal("FORBIDDEN"),
message: z.string(),
}),
}),
})
.get((opts) => {
const { input, ctx } = opts;
const user = findUser(input.id);
if (!user) {
// Can throw 404 error (defined on route)
throw ctx.error({
error: {
code: "NOT_FOUND",
message: "User not found",
},
});
}
if (!canAccessUser(user)) {
// Can throw 403 error (defined on route)
throw ctx.error({
error: {
code: "FORBIDDEN",
message: "Access denied",
},
});
}
if (!isAuthenticated()) {
// Can throw 401 error (inherited from procedure)
throw ctx.error({
error: {
code: "UNAUTHORIZED",
message: "Authentication required",
},
});
}
return user;
}),
});
In this example, the route has access to all three error types:
401 UNAUTHORIZEDfrom the procedure404 NOT_FOUNDfrom the route403 FORBIDDENfrom the route
Union of Error Schemas: If both the procedure and route define an error with the same status code, the schemas are unioned. This means ctx.error() can accept either schema for that status code:
const apiProcedure = publicProcedure.errors({
401: z.object({
error: z.object({
code: z.literal("UNAUTHORIZED"),
message: z.string(),
}),
}),
});
export const settingsRouter = router({
me: apiProcedure
.input({})
.errors({
// Route defines a different 401 error schema - they will be unioned
401: z.object({
error: z.object({
code: z.literal("SESSION_EXPIRED"),
message: z.string(),
redirect: z.string().url(),
}),
}),
})
.get((opts) => {
const { ctx } = opts;
// Can throw 401 with either UNAUTHORIZED or SESSION_EXPIRED
throw ctx.error({
error: {
code: "SESSION_EXPIRED",
message: "Your session has expired",
redirect: "https://example.com/login",
},
});
}),
});
Middleware Chaining
Middleware can be chained to build up context:
import { router, publicProcedure } from "@alt-stack/server-hono";
import { z } from "zod";
interface AppContext {
user: { id: string; role: string } | null;
}
const factory = init<AppContext>();
const authProcedure = factory.procedure.use(async (opts) => {
const { ctx, next } = opts;
const user = await getUser(ctx);
if (!user) {
throw ctx.error({ code: "UNAUTHORIZED" });
}
return next({ ctx: { user } });
});
const adminProcedure = authProcedure.use(async (opts) => {
const { ctx, next } = opts;
if (ctx.user.role !== "admin") {
throw ctx.error({ code: "FORBIDDEN" });
}
return next({ ctx: { isAdmin: true } });
});
export const adminRouter = router({
settings: adminProcedure
.input({})
.get((opts) => {
// opts.ctx.user and opts.ctx.isAdmin are both available
const { ctx } = opts;
return { admin: ctx.isAdmin };
}),
});
Common Patterns
Public and Protected Routes
import { router, publicProcedure, init } from "@alt-stack/server-hono";
import { z } from "zod";
interface AppContext {
user: { id: string; name: string } | null;
}
const factory = init<AppContext>();
const publicProc = publicProcedure;
const protectedProcedure = factory.procedure.use(authMiddleware);
export const appRouter = router({
hello: publicProc.get(() => "hello"),
profile: protectedProcedure
.input({})
.get((opts) => {
return opts.ctx.user!; // Non-null due to middleware
}),
});
Role-Based Procedures
import { router, publicProcedure, init } from "@alt-stack/server-hono";
interface AppContext {
user: { role: string } | null;
}
const factory = init<AppContext>();
const requireRole = (role: string) =>
factory.procedure.use(async (opts) => {
const { ctx, next } = opts;
if (!ctx.user || ctx.user.role !== role) {
return new Response("Forbidden", { status: 403 });
}
return next();
});
const adminProcedure = requireRole("admin");
const moderatorProcedure = requireRole("moderator");
export const adminRouter = router({
delete: adminProcedure.get(() => ({ success: true })),
});
export const moderatorRouter = router({
moderate: moderatorProcedure.post(() => ({ success: true })),
});
Rate Limited Procedures
import { router, publicProcedure } from "@alt-stack/server-hono";
const rateLimitedProcedure = publicProcedure.use(async (opts) => {
const { ctx, next } = opts;
const rateLimitKey = getRateLimitKey(ctx);
if (await isRateLimited(rateLimitKey)) {
return new Response("Too many requests", { status: 429 });
}
await incrementRateLimit(rateLimitKey);
return next();
});
export const apiRouter = router({
data: rateLimitedProcedure.get(() => {
return { data: [] };
}),
});