Routing
Learn how file-based routing works in Lithia
Introduction
Lithia uses file-based routing - your folder structure automatically becomes your API structure. No route configuration needed.
With Express:
const express = require('express');
const app = express();
app.get('/users', getUsers);
app.post('/users', createUser);
app.get('/users/:id', getUserById);
app.put('/users/:id', updateUser);
app.delete('/users/:id', deleteUser);
// Repeat for every endpoint...With Lithia:
src/routes/
└── users/
├── route.get.ts → GET /users
├── route.post.ts → POST /users
└── [id]/
├── route.get.ts → GET /users/:id
├── route.put.ts → PUT /users/:id
└── route.delete.ts → DELETE /users/:idJust create files. They become routes automatically.
Basic Routing
Simple route
Create a file and export a handler:
import type { LithiaRequest, LithiaResponse } from 'lithia';
export default async (_req: LithiaRequest, res: LithiaResponse) => {
return res.json({ message: "Hello!" });
};This creates: GET /hello
All methods route
Use route.ts (without method suffix) to handle all HTTP methods:
import type { LithiaRequest, LithiaResponse } from 'lithia';
export default async (req: LithiaRequest, res: LithiaResponse) => {
switch (req.method) {
case 'GET':
return res.json({ users: [] });
case 'POST':
return res.json({ created: true });
default:
return res.status(405).json({ error: 'Method not allowed' });
}
};This handles: GET /users, POST /users, PUT /users, etc.
We don't know if someone will really use it, but you can if you want, and it's cool :)
Method-Specific Routes
Use route.{method}.ts to handle only specific HTTP methods:
Supported methods
| File name | HTTP method | Example URL |
|---|---|---|
route.get.ts | GET | GET /users |
route.post.ts | POST | POST /users |
/route.put.ts | PUT | PUT /users/:id |
/route.delete.ts | DELETE | DELETE /users/:id |
/route.patch.ts | PATCH | PATCH /users/:id |
Example: CRUD operations
// GET /posts - List all posts
export default async (_req, res) => {
const posts = await db.posts.findMany();
return res.json(posts);
};// POST /posts - Create new post
export default async (req, res) => {
const { title, content } = await req.body();
const post = await db.posts.create({ title, content });
return res.status(201).json(post);
};// PUT /posts/:id - Update post
export default async (req, res) => {
const { id } = req.params;
const { title, content } = await req.body();
const post = await db.posts.update(id, { title, content });
return res.json(post);
};// DELETE /posts/:id - Delete post
export default async (req, res) => {
const { id } = req.params;
await db.posts.delete(id);
return res.status(204).end();
};When to use each:
- Use
route.{method}.tswhen you have different logic per method (most common) - Use
route.tswhen logic is shared across methods or for simple routing
Dynamic Routes
Use [param] syntax for dynamic URL parameters:
import type { LithiaRequest, LithiaResponse } from 'lithia';
import { db } from '@/db';
export default async (req: LithiaRequest, res: LithiaResponse) => {
const { id } = req.params;
const user = await db.users.findById(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
return res.json(user);
};This matches: GET /users/123, GET /users/abc, etc.
Access params:
const { id } = req.params; // "123" from /users/123Multiple parameters
src/routes/
└── posts/
└── [postId]/
└── comments/
└── [commentId]/
└── route.get.ts → GET /posts/:postId/comments/:commentIdexport default async (req, res) => {
const { postId, commentId } = req.params;
const comment = await db.comments.findOne({
where: { postId, id: commentId }
});
return res.json(comment);
};Access: GET /posts/42/comments/7
req.params.postId="42"req.params.commentId="7"
Nested Routes
Organize related endpoints with nested folders:
src/routes/
└── users/
├── route.get.ts → GET /users
├── route.post.ts → POST /users
├── [id]/
│ ├── route.get.ts → GET /users/:id
│ ├── route.put.ts → PUT /users/:id
│ ├── route.delete.ts → DELETE /users/:id
│ └── profile/
│ ├── route.get.ts → GET /users/:id/profile
│ └── route.put.ts → PUT /users/:id/profile
└── me/
└── route.get.ts → GET /users/meThe path is built automatically from the folder structure.
Route Priority
When multiple routes could match the same URL, Lithia follows this priority:
- Static routes (exact match)
- Dynamic routes (with parameters)
Example:
users/
├── me/
│ └── route.get.ts → GET /users/me (priority 1)
└── [id]/
└── route.get.ts → GET /users/:id (priority 2)Request to GET /users/me:
- Matches
/users/mefirst (static wins) - Never reaches
/users/[id]
Request to GET /users/123:
- Doesn't match
/users/me - Matches
/users/[id]withid = "123"
Best Practices
Lithia.js focuses on functional programming, but we don't need to overcomplicate things:
- Method-specific routes - Use
route.{method}.tsinstead of handling everything inroute.ts - Keep routes thin - Move business logic to services
- Static before dynamic - Create
/users/mebefore/users/[id] - Consistent naming - Use plural for collections (
/users, not/user)
Troubleshooting
Route module must export a default function
If you get this error on request:
{
"error": {
"name": "InternalServerError",
"status": 500,
"message": "Invalid module: Route module must export a default function"
}
}It means your route file is not exporting a default function. You need to export a default function.
export default async (req, res) => {
// your code here
};Route handler must be an async function
If you get this error:
{
"error": {
"name": "InternalServerError",
"status": 500,
"message": "Invalid module: Route handler must be an async function"
}
}It means your route file is not an async function. You need to make it an async function.
export default async (req, res) => {
// your code here
};