Routing
Balda provides flexible routing through direct server registration, the router singleton, or controller decorators.
Route Registration Methods
1. Direct Server Routes
import { Server, router } from "balda";
import { z } from "zod";
const server = new Server({ port: 3000 });
// Simple routes
router.get("/users", (req, res) => res.json({ users: [] }));
router.post("/users", (req, res) => res.created(req.body));
router.put("/users/:id", (req, res) => res.json({ id: req.params.id }));
router.delete("/users/:id", (req, res) => res.noContent());
// With inline validation - validated data is written to req.body / req.query
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
router.post(
"/users/validated",
{
body: CreateUserSchema,
},
(req, res) => {
// req.body is validated and typed
res.created({ id: "123", ...req.body });
},
);
// With options
router.post(
"/users",
{
middlewares: [authMiddleware],
body: CreateUserSchema,
responses: {
201: UserResponseSchema,
},
swagger: {
name: "Create User",
},
},
(req, res) => {
res.created(req.body);
},
);
2. Router Singleton
Useful for modular route definitions:
import { router } from "balda";
import { z } from "zod";
// Simple routes
router.get("/health", (req, res) => res.json({ status: "ok" }));
// With inline validation
const LoginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
router.post(
"/login",
{
body: LoginSchema,
},
(req, res) => {
// req.body is validated and typed
const { email, password } = req.body;
res.json({ token: generateToken(email, password) });
},
);
// Query validation
const SearchSchema = z.object({
q: z.string(),
page: z.coerce.number().default(1),
});
router.get(
"/search",
{
query: SearchSchema,
},
(req, res) => {
// query parameters are validated and type-coerced
res.json({ results: [], query: req.query.q, page: req.query.page });
},
);
3. Controller Decorators
Recommended for organized, feature-based routing:
import { controller, get, post } from "balda";
@controller("/users")
export class UsersController {
@get("/") getAll(req, res) {
res.json({ users: [] });
}
@get("/:id") getById(req, res) {
res.json({ id: req.params.id });
}
@post("/") create(req, res) {
res.created(req.body);
}
}
See Controllers for detailed controller documentation.
Route Parameters
Path Parameters
// Single parameter
router.get("/users/:id", (req, res) => {
res.json({ id: req.params.id });
});
// Multiple parameters
router.get("/users/:userId/posts/:postId", (req, res) => {
const { userId, postId } = req.params;
res.json({ userId, postId });
});
Query Parameters
router.get("/users", (req, res) => {
const { page = 1, limit = 10, search } = req.query;
res.json({ page: Number(page), limit: Number(limit), search });
});
Route Patterns
// Static route
router.get("/about", (req, res) => res.json({ message: "About" }));
// Dynamic parameter
router.get("/users/:id", (req, res) => res.json({ id: req.params.id }));
// Optional parameter
router.get("/posts/:id?", (req, res) => {
req.params.id ? res.json({ id }) : res.json({ posts: [] });
});
// Wildcard
router.get("/files/*", (req, res) => {
res.json({ filePath: req.params["*"] });
});
Route Precedence
Routes are matched in registration order. Define specific routes before general ones:
// Specific first
router.get("/users/admin", (req, res) => res.json({ admin: true }));
// General after
router.get("/users/:id", (req, res) => res.json({ id: req.params.id }));
Type-Safe Routing
Balda provides automatic type inference for both path parameters and response bodies.
Path Parameters
Path parameters are automatically inferred from the route path string:
// Path parameters are inferred — no manual typing needed
router.get('/users/:id', (req, res) => {
const { id } = req.params; // ✅ Typed as { id: string }
res.json({ id });
});
Type-Safe Responses
When you define response schemas in the responses option, Balda automatically infers the response body types for each status code. This works with Zod, TypeBox, and plain JSON schemas:
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
});
const ErrorSchema = z.object({
error: z.string(),
});
router.get(
'/users/:id',
{
responses: {
200: UserSchema,
404: ErrorSchema,
},
},
(req, res) => {
const { id } = req.params; // ✅ Typed as { id: string }
res.ok({ id, name: "John", email: "john@example.com" }); // ✅ Typed as { id: string; name: string; email: string }
res.notFound({ error: "User not found" }); // ✅ Typed as { error: string }
// res.ok({ wrong: 123 }); // ❌ Type error — doesn't match UserSchema
// res.notFound({ wrong: "value" }); // ❌ Type error — doesn't match ErrorSchema
},
);
Each shorthand method (ok(), created(), notFound(), etc.) is typed to its corresponding status code schema. Methods for unmapped status codes default to any.
TypeBox schemas provide the same type inference as Zod via Static<T>. Plain JSON Schema objects also support full type inference via json-schema-to-ts — as const is required so TypeScript preserves the literal types needed for inference.
Also, you need to install json-schema-to-ts to be able to use plain JSON schemas with type inference.
npm install json-schema-to-ts --save-dev
const UserSchema = {
type: "object",
properties: {
id: { type: "integer" },
name: { type: "string" },
},
required: ["id", "name"],
additionalProperties: false,
} as const; // ← required for type inference
router.get("/users/:id", { responses: { 200: UserSchema } }, (req, res) => {
res.ok({ id: 1, name: "Alice" }); // ✅ typed as { id: number; name: string }
});
Without as const, plain JSON schema objects resolve to any.
Controller Type Safety
For controller-based routes, you can manually type the Response generic:
import { Request, Response } from 'balda';
type UserResponse = { id: string; name: string; email: string };
@post('/')
@validate.body(CreateUserSchema)
async create(
req: Request<{}>,
res: Response<{ 201: UserResponse }>,
body: z.infer<typeof CreateUserSchema> // ✅ Validated & typed
) {
res.created({ id: '123', ...body }); // ✅ Typed as UserResponse
}
See Request-Response for more on type-safe requests and responses.
Route Options
All route methods (get, post, put, patch, delete, options, head) support an optional configuration object:
import { z } from "zod";
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
});
const QuerySchema = z.object({
includeDeleted: z.coerce.boolean().default(false),
});
router.post(
"/users",
{
// Request validation — inline route validation writes validated data to `req`
body: UserSchema, // Validates and sets `req.body`
query: QuerySchema, // Validates and sets `req.query`
all: CombinedSchema, // Validates body+query merged (mutually exclusive with body/query)
// Middleware
middlewares: [authMiddleware, validationMiddleware], // or single middleware
// Swagger documentation
swagger: {
name: "Create User",
description: "Creates a new user",
responses: {
201: UserResponseSchema,
400: ErrorSchema,
},
},
},
(req, res) => {
// req.body and req.query are typed and validated
res.created({ id: "123", ...req.body });
},
);
Validation Options
body: Validates the request body and writes the typed result toreq.body.query: Validates the query string and writes the typed result toreq.query.all: Validates a merged object composed of body + query and writes the result toreq.body(cannot be used withbodyorquery).
When you specify body or query at the route level, they're automatically included in Swagger documentation. No need to specify requestBody or query in the swagger options.
Route Groups
Use router.group() to organize related routes:
import { z } from "zod";
// Basic grouping
router.group("/api/v1", (r) => {
r.get("/users", (req, res) => res.json({ users: [] }));
r.get("/posts", (req, res) => res.json({ posts: [] }));
});
// With middleware
router.group("/admin", [authMiddleware, adminMiddleware], (r) => {
r.get("/dashboard", (req, res) => res.json({ ok: true }));
});
// With validation in grouped routes
router.group("/api/v2", (r) => {
const CreatePostSchema = z.object({
title: z.string(),
content: z.string(),
});
r.post(
"/posts",
{
body: CreatePostSchema,
middlewares: [authMiddleware],
},
(req, res, validatedBody) => {
res.created({ id: "123", ...validatedBody });
},
);
});
See the Server documentation for more details on grouping and middleware.