Routing
Lacis generates routes automatically from your routes/ directory. Every index.ts file inside that directory becomes a URL endpoint — no manual route registration needed.
Directory structure
Section titled “Directory structure”Place route files under routes/. The directory hierarchy maps directly to URL paths.
Directoryroutes/
- index.ts → GET /
Directoryusers/
- index.ts → /users
Directory[id]/
- index.ts → /users/:id
Directoryposts/
- index.ts → /posts
Directory[slug]/
- index.ts → /posts/:slug
HTTP method exports
Section titled “HTTP method exports”Export named functions matching uppercase HTTP method names. Each export handles that method for the file’s URL.
// routes/users/index.ts → /usersimport type { Request, Response } from 'lacis'
export async function GET(req: Request, res: Response) { res.json({ users: [] })}
export async function POST(req: Request, res: Response) { const body = await req.json() res.status(201).json({ created: body })}
export async function PUT(req: Request, res: Response) { const body = await req.json() res.json({ updated: body })}
export async function PATCH(req: Request, res: Response) { const body = await req.json() res.json({ patched: body })}
export async function DELETE(req: Request, res: Response) { res.status(204).send('')}Supported method exports: GET, POST, PUT, PATCH, DELETE.
Dynamic segments
Section titled “Dynamic segments”Wrap a directory name in square brackets to make it a URL parameter. The parameter name is the text inside the brackets.
Directoryroutes/
Directoryusers/
Directory[id]/
- index.ts → /users/:id
Directoryorgs/
Directory[orgId]/
Directoryteams/
Directory[teamId]/
- index.ts → /orgs/:orgId/teams/:teamId
Access parameters via req.params:
// routes/users/[id]/index.ts → /users/:idimport type { Request, Response } from 'lacis'
export async function GET(req: Request, res: Response) { const { id } = req.params res.json({ id })}Typed params from the folder (./$types)
Section titled “Typed params from the folder (./$types)”lacis dev and lacis build generate a per-route $types file so req.params is typed
from the folder name — no schema, no defineHandler required:
// routes/users/[id]/index.tsimport type { RouteHandler } from './$types'
export const GET: RouteHandler = async (req, res) => { req.params.id // string — inferred from [id] ([id?] → string | undefined) res.json({ id: req.params.id })}This relies on TypeScript’s rootDirs, which the create-lacis scaffold configures for you:
// tsconfig.json{ "compilerOptions": { "rootDirs": [".", "./.lacis/types"] } }The generated .lacis/ directory is a build artifact — keep it out of version control
(.gitignore). For runtime validation of params (and a typed response), combine this with
defineHandler’s params schema.
Nested routes
Section titled “Nested routes”Nest directories to create nested URL paths. Each level can have its own index.ts with independent handlers.
Directoryroutes/
Directoryapi/
- index.ts → /api
Directoryusers/
- index.ts → /api/users
Directory[id]/
- index.ts → /api/users/:id
Directoryposts/
- index.ts → /api/users/:id/posts
// routes/api/users/[id]/posts/index.ts → /api/users/:id/postsimport type { Request, Response } from 'lacis'
export async function GET(req: Request, res: Response) { const { id } = req.params res.json({ userId: id, posts: [] })}Wildcard routes
Section titled “Wildcard routes”Use a [...] directory name to match any remaining path segments.
Directoryroutes/
Directoryfiles/
Directory[…path]/
- index.ts → /files/*
// routes/files/[...path]/index.tsimport type { Request, Response } from 'lacis'
export async function GET(req: Request, res: Response) { // the matched remainder is exposed under the "*" key const filePath = req.params['*'] res.json({ path: filePath })}Method Not Allowed
Section titled “Method Not Allowed”When a route exists but the request uses an unregistered HTTP method, Lacis returns 405 Method Not Allowed and sets an Allow header listing the supported methods automatically.
Complete example
Section titled “Complete example”// routes/index.ts → GET /import type { Request, Response } from 'lacis'
export async function GET(req: Request, res: Response) { res.json({ name: 'My API', version: '1.0.0' })}// routes/users/index.ts → /usersimport type { Request, Response } from 'lacis'
export async function GET(req: Request, res: Response) { res.json({ users: [] })}
export async function POST(req: Request, res: Response) { const body = await req.json<{ name: string; email: string }>() res.status(201).json({ id: crypto.randomUUID(), ...body })}// routes/users/[id]/index.ts → /users/:idimport type { Request, Response } from 'lacis'
export async function GET(req: Request, res: Response) { const { id } = req.params res.json({ id, name: 'Alice' })}
export async function DELETE(req: Request, res: Response) { res.status(204).send('')}