Lithia

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:

app.js
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/
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

Just create files. They become routes automatically.

Basic Routing

Simple route

Create a file and export a handler:

src/routes/hello/route.get.ts
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:

src/routes/users/route.ts
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 nameHTTP methodExample URL
route.get.tsGETGET /users
route.post.tsPOSTPOST /users
/route.put.tsPUTPUT /users/:id
/route.delete.tsDELETEDELETE /users/:id
/route.patch.tsPATCHPATCH /users/:id

Example: CRUD operations

src/routes/posts/route.get.ts
// GET /posts - List all posts
export default async (_req, res) => {
  const posts = await db.posts.findMany();
  return res.json(posts);
};
src/routes/posts/route.post.ts
// 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);
};
src/routes/posts/[id]/route.put.ts
// 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);
};
src/routes/posts/[id]/route.delete.ts
// 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}.ts when you have different logic per method (most common)
  • Use route.ts when logic is shared across methods or for simple routing

Dynamic Routes

Use [param] syntax for dynamic URL parameters:

src/routes/users/[id]/route.get.ts
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:

example
const { id } = req.params;  // "123" from /users/123

Multiple parameters

src/routes/
src/routes/
└── posts/
    └── [postId]/
        └── comments/
            └── [commentId]/
                └── route.get.ts  → GET /posts/:postId/comments/:commentId
route.get.ts
export 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/
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/me

The path is built automatically from the folder structure.

Route Priority

When multiple routes could match the same URL, Lithia follows this priority:

  1. Static routes (exact match)
  2. Dynamic routes (with parameters)

Example:

src/routes/users/
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/me first (static wins)
  • Never reaches /users/[id]

Request to GET /users/123:

  • Doesn't match /users/me
  • Matches /users/[id] with id = "123"

Best Practices

Lithia.js focuses on functional programming, but we don't need to overcomplicate things:

  1. Method-specific routes - Use route.{method}.ts instead of handling everything in route.ts
  2. Keep routes thin - Move business logic to services
  3. Static before dynamic - Create /users/me before /users/[id]
  4. 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
{
  "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.

route.ts
export default async (req, res) => {
  // your code here
};

Route handler must be an async function

If you get this error:

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.

route.ts
export default async (req, res) => {
  // your code here
};