# How to Customize JSON Server with Custom Routes and Middleware

> Learn how to customize JSON Server with custom routes and middleware. Easily extend your mock API using the createApp function and Tinyhttp compatible middleware for flexible development.

- Repository: [typicode/json-server](https://github.com/typicode/json-server)
- Tags: how-to-guide
- Published: 2026-03-01

---

**You can customize JSON Server by importing the `createApp` function from [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts), adding Tinyhttp-compatible middleware and routes to the returned app instance, and then calling `app.listen()` to start the server.**

JSON Server provides an instant REST API based on a JSON file, but its real power lies in extensibility. Because the core server is built on Tinyhttp (an Express-compatible framework) and exported via the `createApp` function in `typicode/json-server`, you can inject custom middleware and routes before the generic CRUD handlers take over.

## Understanding JSON Server Architecture

Before adding custom code, you need to understand how the server is constructed. The library exposes a factory function rather than a singleton, allowing you to modify the app instance before it starts listening.

### Core Server Construction in [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts)

The `createApp` function (lines 90‑165 of [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts)) constructs the Tinyhttp `App` instance and wires all default middleware. It accepts a `Low<Data>` database instance and an optional `AppOptions` object.

The function registers three critical middleware layers in sequence:

1. **Static file serving** (lines 98‑101) – Uses `sirv` to serve the `public` directory and any additional static paths passed via options.
2. **CORS handling** (lines 103‑112) – Applies `@tinyhttp/cors` with dynamic header detection.
3. **JSON body parser** (line 115) – Uses `milliparsec` to parse request bodies.

Finally, it mounts the generic `/:name` handler (lines 155‑163) that powers the default CRUD API.

### CLI Entry Point in [`src/bin.ts`](https://github.com/typicode/json-server/blob/main/src/bin.ts)

The command‑line interface (lines 42‑45 and 65‑71 of [`src/bin.ts`](https://github.com/typicode/json-server/blob/main/src/bin.ts)) orchestrates startup:

- Parses CLI arguments (file, port, host, static directories).
- Instantiates the `Low` database and reads the JSON file.
- Invokes `createApp(db, options)` to obtain the configured app.
- Calls `app.listen()` to start the HTTP server.

Because the CLI simply calls `createApp`, you can replicate this flow in your own script and modify the returned app before listening.

## Adding Custom Middleware to JSON Server

Because `createApp` returns a raw Tinyhttp `App` instance, you can prepend or append any Express‑compatible middleware. The key is to register your middleware **after** calling `createApp` but **before** `app.listen()`.

### Request Logging Middleware

The following example adds the `@tinyhttp/logger` middleware to trace every request:

```typescript
import { createApp } from './src/app.ts';
import { Low, Memory } from 'lowdb';
import { logger } from '@tinyhttp/logger';
import type { Data } from './src/service.ts';

const db = new Low<Data>(new Memory<Data>(), {});
db.data = { posts: [] };

const app = createApp(db);

// Insert logging before the built-in routes
app.use(logger());

app.listen(3000, () => console.log('Server with logging on port 3000'));

```

Because `logger()` is added after `createApp` returns, it executes before the generic `/:name` handler defined in [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts) (lines 155‑163).

## Creating Custom Routes in JSON Server

Custom routes work the same way as middleware: you attach them to the returned `App` instance. To avoid shadowing the default CRUD endpoints, register specific paths (e.g., `/health`, `/api/stats`) before starting the server.

### Health Check and Aggregation Routes

The following example demonstrates a health endpoint and a custom aggregation that joins posts with comments:

```typescript
import { createApp } from './src/app.ts';
import { Low, Memory } from 'lowdb';
import type { Data } from './src/service.ts';

const db = new Low<Data>(new Memory<Data>(), {});
db.data = {
  posts: [{ id: '1', title: 'First Post' }],
  comments: [{ id: '1', postId: '1', text: 'Great article' }]
};

const app = createApp(db);

// ① Health check bypasses default CRUD
app.get('/health', (_req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// ② Custom aggregation route
app.get('/posts/:id/comments', async (req, res) => {
  const { id } = req.params;
  const post = db.data?.posts?.find((p) => p.id === id);
  
  if (!post) return res.sendStatus(404);
  
  const related = db.data?.comments?.filter((c) => c.postId === id) ?? [];
  res.json({ ...post, comments: related });
});

app.listen(4000, () => console.log('Custom server listening on 4000'));

```

- The `/health` endpoint returns immediately because it is registered before the catch‑all `/:name` handler in [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts) (lines 155‑163).
- The aggregation route demonstrates direct access to the `db.data` object, bypassing the `Service` layer if you need custom business logic.

## Extending Static File Serving

If your custom application requires additional static directories (for a custom UI or documentation), you can pass them via the `static` option in `createApp` or manually mount them afterward.

```typescript
import { createApp } from './src/app.ts';
import { Low, Memory } from 'lowdb';
import type { Data } from './src/service.ts';
import { join } from 'node:path';

const db = new Low<Data>(new Memory<Data>(), {});
db.data = {};

const extraDir = join(process.cwd(), 'my-static');

// Option 1: Pass via createApp options (processed in src/app.ts lines 98-101)
const app = createApp(db, { static: [extraDir] });

// Option 2: Manual mounting after creation
// app.use(sirv(extraDir, { dev: true }));

app.listen(3000);

```

The `static` option is processed inside [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts) (lines 98‑101) using `sirv`, the same static file server used for the built‑in `public` directory.

## Summary

- **Import `createApp`** from [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts) to obtain a configurable Tinyhttp `App` instance instead of using the CLI directly.
- **Register middleware** (logging, authentication, CORS) by calling `app.use()` after `createApp` returns but before `app.listen()`.
- **Add custom routes** (e.g., `/health`, `/api/stats`) to override or extend the default CRUD behavior provided by the `/:name` handler in [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts) (lines 155‑163).
- **Access the database** directly via the `Low<Data>` instance passed to `createApp` for custom aggregation endpoints.
- **Extend static serving** via the `static` option or manual `sirv` mounting to serve custom UIs alongside the API.

## Frequently Asked Questions

### Can I use Express middleware with JSON Server?

Yes. JSON Server is built on Tinyhttp, which implements the same `app.use()` signature as Express. Any middleware compatible with Express 4.x (such as `morgan`, `helmet`, or `compression`) can be registered by calling `app.use(middleware())` after importing `createApp` from [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts) and before starting the server.

### Where should I register custom routes to avoid conflicts with JSON Server's default routes?

Register custom routes **after** calling `createApp` but **before** `app.listen()`. Place specific paths (like `/health` or `/api/v2/users`) before the generic `/:name` handler defined in [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts) (lines 155‑163). Because Tinyhttp matches routes in registration order, your custom endpoints will take precedence over the catch‑all CRUD routes.

### How do I access the database instance in custom routes?

The `createApp` function accepts a `Low<Data>` instance as its first argument. You can capture this reference in your script and query it directly inside custom route handlers. For example, inside an `app.get()` callback, read from `db.data.posts` or any other collection. This bypasses the default `Service` layer when you need custom aggregation or complex queries.

### Can I disable JSON Server's default CRUD routes?

The default routes are mounted via the generic `/:name` handler at the end of `createApp` (lines 155‑163 of [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts)). While there is no built‑in flag to disable them, you can effectively override specific resources by registering custom routes for those paths before the server starts. If you need a completely clean slate, you would need to fork the repository and modify [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts) to remove the default handler, or simply not use `createApp` and build your own Tinyhttp app using the `Service` class from [`src/service.ts`](https://github.com/typicode/json-server/blob/main/src/service.ts).