Routes
Define type-safe HTTP routes with full request validation using Thunder's Router.
Thunder's routing system is built around a clean, functional API that combines Zod validation, TypeScript type safety, and a declarative handler pattern. Every route in Thunder validates its inputs and enforces its output shape at compile time.
Internally, Thunder uses path-to-regexp for URL pattern matching, which means you have the full power of named parameters, optional segments, and wildcard paths available out of the box.
Creating a Router
Use the built-in router snippet (or write it manually) to scaffold a new router file:
import { Router } from "@/core/http/router.ts";
export default new Router("/", function home(router) {
// Use the `req` snippet inside here to add route handlers
});Each router receives a base path as its first argument. All routes defined within that router are mounted relative to that base.
Defining Route Handlers
Inside a router, use the req snippet or write a handler manually. The structure of every handler follows this pattern:
router.get("/", function getName() {
const $params = z.object({});
const $query = z.object({});
const $body = z.object({});
const $return = z.object({});
return {
shape: () => ({
params: $params,
query: $query,
body: $body,
return: $return,
}),
handler: async (req: Request) => {
const params = $params.parse(paramsAsJson(req));
const query = $query.parse(queryAsJson(req));
const body = $body.parse(await bodyAsJson(req));
return Response.json({} satisfies z.output<typeof $return>);
},
};
});What Each Part Does
| Part | Purpose |
|---|---|
$params | Validates URL path parameters (e.g. /users/:id) |
$query | Validates URL query string values (e.g. ?page=1&limit=10) |
$body | Validates the request body (primarily for POST, PUT, PATCH) |
$return | Defines the expected shape of the response - TypeScript enforces this |
shape() | Registers all validators with the framework |
handler | Contains your business logic |
The satisfies z.output<typeof $return> expression at the end of your handler is what makes Thunder's response validation work. TypeScript will raise a compile-time error if the object you return does not match your declared $return schema.
Request Utilities
Import these utilities from @/core/http/utils.ts to extract validated data from the request object:
import { bodyAsJson, paramsAsJson, queryAsJson } from "@/core/http/utils.ts";Always use the @/ alias. Relative imports into core/ are forbidden and will produce a lint error:
Relative import into "core/" is forbidden. Use "@/core/..." instead.HTTP Methods
Thunder supports all standard HTTP methods. Use the following methods on your router instance:
| Method | Router Call | Use Case |
|---|---|---|
| GET | router.get(...) | Retrieve a resource |
| POST | router.post(...) | Create a resource |
| PUT | router.put(...) | Replace a resource |
| PATCH | router.patch(...) | Partially update a resource |
| DELETE | router.del(...) | Delete a resource |
Use .del() - not .delete() - for DELETE endpoints. .delete is a reserved keyword in JavaScript.
URL Parameters
Path parameters are defined using the path-to-regexp syntax and validated via $params:
router.get("{/:id}", function getUser() {
const $params = z.object({
id: z.string(),
});
return {
shape: () => ({ params: $params }),
handler: async (req: Request) => {
const { id } = $params.parse(paramsAsJson(req));
// use id...
return Response.json({ id });
},
};
});Response Helpers
Thunder extends the native Response object with static helper methods for common HTTP responses:
return Response.ok(); // 200 OK
return Response.created(data); // 201 Created
return Response.unauthorized(); // 401 UnauthorizedComplete Method Examples
router.get("{/:id}", function getEnv() {
const $params = z.object({
id: z.string(),
});
const $return = z.object({
key: z.string(),
value: z.string(),
});
return {
shape: () => ({ params: $params, return: $return }),
handler: async (req: Request) => {
const { id } = $params.parse(paramsAsJson(req));
const doc = await envModel.findOne({ _id: new ObjectId(id) });
return Response.json(doc satisfies z.output<typeof $return>);
},
};
});router.post("/", function createEnv() {
const $body = z.object({
key: z.string(),
value: z.string(),
});
const $return = z.object({
_id: z.instanceof(ObjectId),
});
return {
shape: () => ({ body: $body, return: $return }),
handler: async (req: Request) => {
const body = $body.parse(await bodyAsJson(req));
const { insertedId } = await envModel.insertOne(body);
return Response.created({ _id: insertedId.toString() });
},
};
});router.patch("{/:id}", function updateEnv() {
const $params = z.object({ id: z.string() });
const $body = z.object({ value: z.string().optional() });
const $return = z.object({ updated: z.boolean() });
return {
shape: () => ({ params: $params, body: $body, return: $return }),
handler: async (req: Request) => {
const { id } = $params.parse(paramsAsJson(req));
const body = $body.parse(await bodyAsJson(req));
await envModel.updateOne(
{ _id: new ObjectId(id) },
{ $set: body }
);
return Response.ok({ updated: true } satisfies z.output<typeof $return>);
},
};
});router.del("{/:id}", function deleteEnv() {
const $params = z.object({ id: z.string() });
const $return = z.object({ deleted: z.boolean() });
return {
shape: () => ({ params: $params, return: $return }),
handler: async (req: Request) => {
const { id } = $params.parse(paramsAsJson(req));
await envModel.deleteOne({ _id: new ObjectId(id) });
return Response.ok({ deleted: true } satisfies z.output<typeof $return>);
},
};
});Error Handling
Throw standard JavaScript errors to signal failures. Thunder will catch these and respond appropriately:
handler: async (req: Request) => {
const doc = await userModel.findOne({ _id: new ObjectId(id) });
if (!doc) {
throw new Error("User not found");
}
return Response.json(doc);
},Use the built-in Logger for structured output:
import { Logger } from "@/core/logger.ts";
Logger.success("Record inserted");
Logger.info("Processing request...");
Logger.warn("Deprecated field used");
Logger.error("Database connection failed");You may also leverage Hooks for centralized error handling and response modification across all routes.