How to Customize JSON Server with Custom Routes and Middleware
You can customize JSON Server by importing the createApp function from 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
The createApp function (lines 90‑165 of 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:
- Static file serving (lines 98‑101) – Uses
sirvto serve thepublicdirectory and any additional static paths passed via options. - CORS handling (lines 103‑112) – Applies
@tinyhttp/corswith dynamic header detection. - JSON body parser (line 115) – Uses
milliparsecto 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
The command‑line interface (lines 42‑45 and 65‑71 of src/bin.ts) orchestrates startup:
- Parses CLI arguments (file, port, host, static directories).
- Instantiates the
Lowdatabase 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:
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 (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:
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
/healthendpoint returns immediately because it is registered before the catch‑all/:namehandler insrc/app.ts(lines 155‑163). - The aggregation route demonstrates direct access to the
db.dataobject, bypassing theServicelayer 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.
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 (lines 98‑101) using sirv, the same static file server used for the built‑in public directory.
Summary
- Import
createAppfromsrc/app.tsto obtain a configurable TinyhttpAppinstance instead of using the CLI directly. - Register middleware (logging, authentication, CORS) by calling
app.use()aftercreateAppreturns but beforeapp.listen(). - Add custom routes (e.g.,
/health,/api/stats) to override or extend the default CRUD behavior provided by the/:namehandler insrc/app.ts(lines 155‑163). - Access the database directly via the
Low<Data>instance passed tocreateAppfor custom aggregation endpoints. - Extend static serving via the
staticoption or manualsirvmounting 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 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 (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). 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 to remove the default handler, or simply not use createApp and build your own Tinyhttp app using the Service class from src/service.ts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →