# json-server Administrator & Developer Guide

This guide is the operator's and contributor's reference for `json-server` `1.0.0-beta.15`. It covers architecture, every flag and configuration knob, deployment patterns, security posture, performance characteristics, and the internals required to extend or embed the library. If you only need to use the CLI to mock an API for a frontend, the User Guide has everything required.

The codebase analyzed here is the **post-rewrite** v1 line: TypeScript source under `src/`, `@tinyhttp` for the HTTP layer, `lowdb` for persistence, and Node's native `parseArgs` for the CLI. It is intentionally a much smaller surface than `0.17.x` — many features from that line (custom routes, middleware files, `--delay`, `--routes`, plugins) have not been re-introduced and are explicitly out of scope here.

---

## 1. Architecture Overview

`json-server` is a thin HTTP wrapper around an in-memory data store, with a normalizing adapter layer between the store and the on-disk JSON file. The single-file CLI (`src/bin.ts`) wires everything together; the HTTP routes live in `src/app.ts`; the data semantics live in `src/service.ts`. Every other file is a small focused helper.

```text
                          ┌──────────────────────────────────────────────┐
                          │                  HTTP Client                 │
                          │     (browser, curl, Postman, fetch, axios)   │
                          └─────────────────────┬────────────────────────┘
                                                │ HTTP/1.1
                                                ▼
        ┌──────────────────────────────────────────────────────────────────┐
        │                          @tinyhttp/app                           │
        │   middleware chain:  sirv(public)  -> sirv(-s dirs) ->           │
        │                      cors -> milliparsec(json) -> route handlers │
        └─────────────────────────────────────┬────────────────────────────┘
                                              │
                                              ▼
        ┌──────────────────────────────────────────────────────────────────┐
        │                            src/app.ts                            │
        │  GET  /                       -> Eta render(views/index.html)    │
        │  GET  /:name                  -> Service.find()                  │
        │  GET  /:name/:id              -> Service.findById()              │
        │  POST /:name                  -> Service.create()                │
        │  PUT  /:name | /:name/:id     -> Service.update / updateById     │
        │  PATCH /:name | /:name/:id    -> Service.patch  / patchById      │
        │  DELETE /:name/:id            -> Service.destroyById()           │
        │                                                                  │
        │  parseListParams() splits query string:                          │
        │    reserved keys -> _sort / _page / _per_page / _embed / _where  │
        │    everything else -> parseWhere() into a where-clause object    │
        └─────────────────────────────────────┬────────────────────────────┘
                                              │
                                              ▼
        ┌──────────────────────────────────────────────────────────────────┐
        │                          src/service.ts                          │
        │       Class: Service(db: Low<Data>)                              │
        │       Methods: has, find, findById, create, update, updateById,  │
        │                patch, patchById, destroyById                     │
        │       Helpers: matchesWhere, paginate, embed,                    │
        │                nullifyForeignKey, deleteDependents               │
        └─────────────────────────────────────┬────────────────────────────┘
                                              │
                                              ▼
        ┌──────────────────────────────────────────────────────────────────┐
        │                            lowdb Low                             │
        │   in-memory state mirrored to disk on every write                │
        └─────────────────────────────────────┬────────────────────────────┘
                                              │
                                              ▼
        ┌──────────────────────────────────────────────────────────────────┐
        │                src/adapters/observer.ts (Observer)               │
        │     fires onReadStart/onReadEnd/onWriteStart/onWriteEnd hooks    │
        │     used by bin.ts to detect external edits & log new endpoints  │
        └─────────────────────────────────────┬────────────────────────────┘
                                              │
                                              ▼
        ┌──────────────────────────────────────────────────────────────────┐
        │       src/adapters/normalized-adapter.ts (NormalizedAdapter)     │
        │   read():  strip $schema, coerce numeric ids -> strings,         │
        │            generate randomId() for items missing id              │
        │   write(): inject $schema = ./node_modules/json-server/schema.json│
        └─────────────────────────────────────┬────────────────────────────┘
                                              │
                                              ▼
        ┌──────────────────────────────────────────────────────────────────┐
        │                lowdb base adapter (chosen by extname)            │
        │     .json  -> JSONFile                                           │
        │     .json5 -> DataFile  (JSON5.parse / JSON5.stringify)          │
        │   underlying disk write goes through  steno  for atomic rename   │
        └─────────────────────────────────────┬────────────────────────────┘
                                              │
                                              ▼
                                   db.json on disk
                                              ▲
                                              │ chokidar.watch(file)
                                              │ (re-read on external change,
                                              │  unless NODE_ENV=production)
                                              │
                                              └─── editor / human / external script
```

> [!NOTE]
> The adapter chain (Observer → NormalizedAdapter → JSONFile/DataFile) is the load-bearing detail of the architecture. It is the only mechanism by which `db.json` is read or written, which keeps the higher layers (`Service`, route handlers) ignorant of disk semantics. Stable IDs, the `$schema` injection, and the file-watch coordination all live here, not in the routing or service layer.

### Component inventory

| Component | Path | Role |
|-----------|------|------|
| CLI entry point | `src/bin.ts` | Parses `process.argv`, opens the DB, wires the watcher, calls `createApp`, calls `app.listen`. |
| HTTP app builder | `src/app.ts` | Exports `createApp(db, options)`. Mounts middleware and route handlers. |
| Domain service | `src/service.ts` | Class `Service`. Pure data operations on the in-memory `Low<Data>` store. |
| Where-clause matcher | `src/matches-where.ts` | Pure function `matchesWhere(item, where)`. Recursively evaluates the JSON predicate tree. |
| Where-clause parser | `src/parse-where.ts` | Pure function `parseWhere(queryString)`. Translates flat query strings into the predicate tree. |
| Operator catalog | `src/where-operators.ts` | Exports `WHERE_OPERATORS` constant array. Single source of truth for valid operator names. |
| Paginator | `src/paginate.ts` | Pure function `paginate(items, page, perPage)`. Returns `{first, prev, next, last, pages, items, data}`. |
| ID generator | `src/random-id.ts` | `randomBytes(8).toString('base64url')`. |
| Normalizing adapter | `src/adapters/normalized-adapter.ts` | Strips `$schema` on read, ensures string IDs, re-injects `$schema` on write. |
| Observer adapter | `src/adapters/observer.ts` | Wraps any `Adapter<T>` with `onReadStart/End` and `onWriteStart/End` hooks. |
| Index template | `views/index.html` | Eta template rendered at `GET /`. |
| JSON Schema | `schema.json` | Draft-2020-12 schema enforcing top-level keys map to arrays or objects. |
| Type-checker config | `tsconfig.json` | TypeScript compiler settings. |
| Formatter config | `.oxfmtrc.json` | `{ "semi": false, "singleQuote": true }`. |

### Technology stack

| Layer | Choice | Version |
|-------|--------|---------|
| Language | TypeScript | `^5.9.3` |
| HTTP framework | `@tinyhttp/app` | `^3.0.1` |
| CORS | `@tinyhttp/cors` | `^2.0.1` |
| Logger (declared, not used in `createApp`) | `@tinyhttp/logger` | `^2.1.0` |
| Body parser | `milliparsec` | `^5.1.0` |
| Static files | `sirv` | `^3.0.2` |
| Templating (`/` index page) | `eta` | `^4.5.0` |
| Persistence | `lowdb` | `^7.0.1` (via `steno` for atomic writes) |
| Object-path setter | `dot-prop` | `^10.1.0` |
| Multi-key sort | `sort-on` | `^7.0.0` |
| Pluralize/singularize | `inflection` | `^3.0.2` |
| JSON5 reader/writer | `json5` | `^2.2.3` |
| File watcher | `chokidar` | `^5.0.0` |
| Terminal color | `chalk` | `^5.6.2` |
| Test runner | `node:test` (built-in) | — |
| Linter | `oxlint` | `^1.39.0` |
| Formatter | `oxfmt` | `^0.24.0` |

### Data flow — end to end

A typical `GET /posts?views:gt=100&_sort=-views&_page=1&_per_page=2`:

1. `@tinyhttp` matches `app.get('/:name')` with `name=posts`.
2. `parseListParams(req)` builds `{ where: { views: { gt: 100 } }, sort: '-views', page: 1, perPage: 2, embed: undefined }`. Reserved keys (`_sort`, `_page`, `_per_page`, `_embed`, `_where`) are split out from the filter params before `parseWhere` runs.
3. The handler calls `service.find('posts', opts)`, which:
   - Reads `db.data['posts']` (an array of `Item`).
   - Applies `embed()` for each requested relation.
   - Filters with `matchesWhere`.
   - Sorts with `sort-on` against the comma-split sort spec.
   - Returns `paginate(results, 1, 2)` if `_page` is set, otherwise the raw array.
4. The result is stashed at `res.locals['data']` and `next()` is called.
5. The fallback `app.use('/:name', ...)` writes the response: `404` if `data === undefined`, status `201` if the method was `POST`, otherwise `200` with the JSON body.

A `POST /posts` follows the same outline, but the body is parsed by `milliparsec`, validated by the inline `isItem()` check (returning `400 Body must be a JSON object` if it fails), and the resulting item is `await service.create(name, body)`. `service.create` assigns `randomId()`, pushes into the array, calls `await db.write()`, and returns the new item.

A `DELETE /posts/1?_dependent=comments` first removes the matching item, then runs `nullifyForeignKey(db, 'posts', '1')` across every other resource (setting `postId: null` wherever it was `'1'`), then runs `deleteDependents(db, 'posts', ['comments'])` which filters out comments whose `postId` is now `null`. Finally `await db.write()` flushes everything.

---

## 2. System Requirements

| Requirement | Minimum | Recommended | Reason |
|-------------|---------|-------------|--------|
| Node.js | `22.12.0` | `22.x` LTS or newer | Enforced by `package.json` `engines.node`. Several deps (`chokidar@5`, `readdirp@5`) require Node `>=20.19`. |
| OS | Any with Node 22 | Linux/macOS preferred | Pure JS — Windows works fine but path normalization in tests assumes POSIX in a few places. |
| RAM | ~64 MB free | More for larger DBs | Whole DB held in memory, plus framework overhead. |
| Disk | ~25 MB | — | `node_modules` for the package and its tree (`@tinyhttp`, `lowdb`, etc.). |
| Network | None for runtime | Outbound for install | Initial install pulls from npm registry. |
| Filesystem | Standard POSIX semantics | Full inotify/fsevents support | The chokidar watcher relies on filesystem events; some bind-mounts (Docker on macOS, WSL2) suppress them. |

### Production (don't) requirements

`json-server` is not designed to be deployed to production. If you nonetheless run it on a server, plan for:

- Single-process, single-threaded — no clustering, no worker pool.
- Whole-file rewrite on every mutating request — write throughput is bounded by disk I/O for the entire DB, not per-row.
- No auth, rate limiting, or request validation beyond JSON parsing.
- Memory grows linearly with DB size; there is no streaming read path.

For mock APIs in CI or a teammate's reachable laptop, none of these matter. For anything beyond that, see Section 7 Security.

---

## 3. Configuration Reference

The full configuration surface of `json-server` is small enough to fit in one section. Everything is here.

### Configuration files

| File | Format | Location | Purpose |
|------|--------|----------|---------|
| `<file>` (positional CLI arg) | JSON or JSON5 | Anywhere on disk | The database. Required. Top-level keys become resources. |
| `package.json` `engines.node` | JSON | Inside the package | Engine constraint. Set to `>=22.12.0`. Not user-tunable. |
| `schema.json` | JSON Schema 2020-12 | Inside the package (`./node_modules/json-server/schema.json`) | Editor-side validation hook. Auto-injected as `$schema` into your DB file on every write. |
| `.oxfmtrc.json` | JSON | Inside the package | Source-formatter config (`{semi: false, singleQuote: true}`). Build-only. |
| `tsconfig.json` | JSON | Inside the package | TS compiler config. Build-only. |

There is **no user-editable configuration file** for runtime behavior. There is no `json-server.json`, no `.json-serverrc`, no `config/` directory. Behavior changes are made by:

- CLI flags (the `parseArgs` schema in `src/bin.ts`)
- Environment variables (`PORT`, `HOST`, `NODE_ENV`)
- The shape of your data file

> [!IMPORTANT]
> If you are migrating from `0.17.x` documentation, you may see references to `routes.json`, `server.js`, middleware files, and `--routes <path>`. **None of these exist in `1.0.0-beta.15`.** Configuration of custom routes or middleware happens by wrapping `json-server` rather than configuring it.

### Command-line flags — complete

The full schema is the `options` block passed to `node:util.parseArgs` in `src/bin.ts`.

| Flag | Short | parseArgs type | Default | Multiple | Description |
|------|-------|----------------|---------|----------|-------------|
| `--port <port>` | `-p` | `string` (parsed to number) | `process.env['PORT'] ?? '3000'` | no | TCP port. Parsed via `parseInt(values.port)`. |
| `--host <host>` | `-h` | `string` | `process.env['HOST'] ?? 'localhost'` | no | Bind address. |
| `--static <dir>` | `-s` | `string` | `[]` | yes | Each occurrence appends a directory. Path is resolved against `process.cwd()` if not absolute. |
| `--help` | — | `boolean` | undefined | no | Print the inline `help()` message and exit. |
| `--version` | — | `boolean` | undefined | no | Print `pkg.version` (read from `../package.json`) and exit. |
| `--watch` | `-w` | `boolean` | undefined | no | **Deprecated.** Logs a yellow warning. Watching is on by default in non-production. |

Positionals: exactly one — the database file path. `parseArgs({allowPositionals: true})`. If `positionals.length === 0` (or `--help`), the help message is printed and the process exits. Unknown options throw `ERR_PARSE_ARGS_UNKNOWN_OPTION`, which is caught and rendered in red, followed by the help message and `process.exit(1)`.

### Environment variables — complete

| Variable | Type | Default | Used in | Effect |
|----------|------|---------|---------|--------|
| `PORT` | string (parsed to number) | none | `src/bin.ts:46` (CLI default) | Default value for `-p` if no flag is passed. CLI flag wins. |
| `HOST` | string | none | `src/bin.ts:51` (CLI default) | Default value for `-h` if no flag is passed. CLI flag wins. |
| `NODE_ENV` | string | none | `src/app.ts:16`, `src/bin.ts:178` | Compared `=== 'production'`. When `production`: (1) Eta template cache is enabled; (2) `sirv` `dev: false` mode is used (HTTP caching headers, no live-reload affordances); (3) the chokidar watcher in `bin.ts` is **not started**. |

> [!NOTE]
> No other environment variables are read by `json-server`. There is no `JSON_SERVER_*` prefix, no `.env` loading, no dotenv-style overrides.

### Reserved query parameters — complete

These five (six counting `_dependent` on DELETE) are recognized by route handlers and never passed through to filters.

| Param | Routes | Type | Default | Purpose |
|-------|--------|------|---------|---------|
| `_sort` | `GET /:name` | string (comma-separated) | (no sort) | Multi-key sort. Prefix with `-` for descending. Forwarded to `sort-on(items, key.split(','))`. |
| `_page` | `GET /:name` | integer | (no pagination) | Page number, 1-indexed. Triggers the wrapped pagination response shape. |
| `_per_page` | `GET /:name` | integer | `10` (when `_page` is set) | Page size. Clamped to ≥1. |
| `_embed` | `GET /:name`, `GET /:name/:id` | string or repeated | (no embed) | Embed related resources. May be repeated. Both singular (parent) and plural (children) forms supported. |
| `_where` | `GET /:name` | URL-encoded JSON | (parsed from other params) | JSON predicate tree. **Overrides** the where-clause computed from regular query params if present and parses as a non-null object. |
| `_dependent` | `DELETE /:name/:id` | string or repeated | (just nullify FKs) | Names additional resources whose foreign-key-bearing rows should be deleted (cascade) instead of orphaned. |

### Where operators — complete

The single source of truth is `src/where-operators.ts`:

```typescript
export const WHERE_OPERATORS = [
  'lt', 'lte', 'gt', 'gte', 'eq', 'ne',
  'in', 'contains', 'startsWith', 'endsWith',
] as const
```

| Operator | Comparison | Notes |
|----------|------------|-------|
| `eq` | `field === value` | Default if no operator is specified. |
| `ne` | `field !== value` | |
| `lt` | `field < value` | JavaScript `<`. Works for numbers, strings (lexicographic), booleans (cast). |
| `lte` | `field <= value` | |
| `gt` | `field > value` | |
| `gte` | `field >= value` | |
| `in` | `value.split(',').includes(field)` | Comma-separated list. Each element is value-coerced (numbers/booleans/null). |
| `contains` | `field.toLowerCase().includes(value.toLowerCase())` | String only. Returns `false` if `field` is not a string. |
| `startsWith` | `field.toLowerCase().startsWith(value.toLowerCase())` | String only. |
| `endsWith` | `field.toLowerCase().endsWith(value.toLowerCase())` | String only. |
| `or` | logical OR over an array of sub-queries | Only valid inside a `_where` JSON object: `{or: [{a:{eq:1}}, {b:{eq:2}}]}`. Not exposed as `field:or=...` — it's a top-level key of the `where` object. |

Operators not in this list are silently dropped during parsing (see `parseWhere` in `src/parse-where.ts:21-23`). This is the cause of issue [#1731](https://github.com/typicode/json-server/issues/1731).

### Value coercion in `parseWhere`

The `coerceValue` helper in `src/parse-where.ts` runs on every operand from a query string:

| Input | Output |
|-------|--------|
| `"true"` | `true` |
| `"false"` | `false` |
| `"null"` | `null` |
| `""` (whitespace-only) | `""` (kept as string) |
| `"42"`, `"3.14"`, `"-1"` | `42`, `3.14`, `-1` (any `Number.isFinite` result) |
| anything else | the original string |

> [!IMPORTANT]
> A query value of `"40123456"` becomes the **number** `40123456` after coercion. If your stored `id` is the string `"40123456"`, an `eq` comparison will fail because the operator implementation uses strict `===`. Track this gotcha if random base64url IDs happen to be all-digit strings (issue [#1734](https://github.com/typicode/json-server/issues/1734)).

### Programmatic configuration

If you embed `json-server` into a larger Node program:

```typescript
import { createApp } from 'json-server/lib/app.js'  // post-build path
import { Low, Memory } from 'lowdb'

const db = new Low(new Memory(), { posts: [], comments: [] })
const app = createApp(db, {
  logger: false,           // boolean, not currently honored beyond shape
  static: ['./public2'],   // string[], same semantics as -s flag
})

app.listen(3000)
```

`AppOptions` (from `src/app.ts:18`):

```typescript
export type AppOptions = {
  logger?: boolean
  static?: string[]
}
```

`logger` is a declared option but not currently consumed by any branch in `createApp` — passing it has no effect. `static` is the equivalent of one or more `-s` flags.

---

## 4. Deployment & Scaling

`json-server` is not built for horizontal scaling. The "deployment" question is mostly about how to keep one process running where you need it, with clear awareness of its limits.

### Local-only (the default and recommended posture)

```bash
npx json-server db.json
```

Bound to `localhost:3000`, serving one developer's machine. Stop with Ctrl-C. This is the design center.

### LAN-reachable

```bash
HOST=0.0.0.0 PORT=4000 npx json-server db.json
```

For mobile-device testing or pairing with a colleague. Keep on a trusted network — see Security.

### Behind a reverse proxy

The only viable production-shaped deployment. Run `json-server` on `localhost`, and front it with a real HTTP server that adds the missing concerns: TLS, auth, rate limiting, logging, route prefixing, latency injection.

Caddy:

```caddyfile
api.example.com {
    basicauth /* {
        admin <bcrypt-hash>
    }
    reverse_proxy localhost:3000
}
```

Nginx:

```nginx
server {
    listen 443 ssl http2;
    server_name api.example.com;

    auth_basic           "json-server";
    auth_basic_user_file /etc/nginx/.htpasswd;

    location /api/ {
        proxy_pass http://127.0.0.1:3000/;
    }
}
```

Vite dev server (in a frontend project):

```javascript
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
}
```

### Containerized (development only)

There is no official Dockerfile. A reasonable one:

```dockerfile
FROM node:22-alpine
WORKDIR /app
RUN npm install -g json-server
COPY db.json ./db.json
EXPOSE 3000
CMD ["json-server", "-h", "0.0.0.0", "db.json"]
```

```bash
docker build -t myapp-mock .
docker run --rm -p 3000:3000 -v $PWD/db.json:/app/db.json myapp-mock
```

> [!WARNING]
> Bind-mounting `db.json` into the container is fine for dev, but the file watcher may not fire on every host filesystem (Docker for Mac, WSL2). If watch isn't reliable, edit the file from inside the container or restart the process after edits.

### CI / automated tests

Run as a backgrounded process in your test job:

```bash
npx json-server -p 4000 fixtures/db.json &
SERVER_PID=$!
trap "kill $SERVER_PID" EXIT

# Wait for it to be ready
until curl -fs http://localhost:4000/ > /dev/null; do sleep 0.2; done

# Run tests
npm test
```

Or use `concurrently` (already a devDep of the project):

```bash
npx concurrently \
  "npx json-server -p 4000 fixtures/db.json" \
  "npm test"
```

### Scaling — what's possible and what isn't

| Want | Feasible? | How |
|------|-----------|-----|
| Multiple instances on different ports | Yes | Start each with a different `-p` and a different `<file>`. They are independent processes with independent state. |
| Two instances sharing one `db.json` | **No** | Two writers will corrupt the file. `lowdb` doesn't coordinate cross-process writes. |
| Read-replicas | **No** | No replication mechanism. |
| Cluster mode (one DB, N workers) | **No** | The state is a per-process in-memory object; there is no shared store. |
| Sticky sessions | N/A | No session state. |
| Vertical scaling (bigger box) | Yes | Helps for read-heavy workloads. Write throughput is bounded by single-threaded full-file rewrites. |

The honest answer: if you find yourself wanting to scale `json-server` past one process, you've outgrown it. Move to a real backend.

---

## 5. Operations

Run-the-thing concerns. Most are short because the surface is small.

### Starting and stopping

```bash
# foreground
npx json-server db.json

# backgrounded (POSIX)
nohup npx json-server db.json > json-server.log 2>&1 &
echo $! > json-server.pid

# stop
kill $(cat json-server.pid)
```

The process responds to `SIGINT` (Ctrl-C) and `SIGTERM` cleanly — `app.listen` returns a Node `http.Server` with default close semantics.

### Health checking

There is no dedicated health-check endpoint. The cheapest probe is a `GET /` (always returns the rendered index page if the DB loaded successfully):

```bash
curl -fs http://localhost:3000/ > /dev/null && echo OK
```

Or any known resource:

```bash
curl -fs http://localhost:3000/posts > /dev/null && echo OK
```

> [!TIP]
> If you're behind a reverse proxy that needs a strict 200-only health check, expose a tiny static file: `mkdir -p public && touch public/healthz` and probe `GET /healthz`.

### Logging

There is no built-in request log. The startup banner is the only log line you get out of the box. To add request logging:

- Run behind a reverse proxy (Caddy/nginx/Traefik all log access by default).
- Or wrap programmatically and chain in `@tinyhttp/logger` before `app.listen`:

  ```typescript
  import { logger } from '@tinyhttp/logger'
  // ...
  const app = createApp(db, options)
  app.use(logger())
  app.listen(port, ...)
  ```

  Note: the order of `app.use` matters with `@tinyhttp` — register the logger after `createApp` returns and before the implicit fallthrough handler runs.

### Backups

The DB is a single file. Backups are file copies.

```bash
# snapshot
cp db.json "backups/db-$(date +%Y%m%d-%H%M%S).json"

# scheduled snapshot every 5 minutes (cron)
*/5 * * * * cp /srv/json-server/db.json /srv/json-server/backups/db-$(date +\%Y\%m\%d-\%H\%M).json
```

> [!NOTE]
> `lowdb`'s underlying writer (`steno`) writes to a temp file and then atomically renames it over the target. Even if you copy mid-write, you get either the pre-write or post-write content, never a torn file. Restore is `cp` in reverse.

### Restoring

```bash
# stop the server first to avoid re-saving stale in-memory state
kill $(cat json-server.pid)
cp backups/db-20260428-120000.json db.json
nohup npx json-server db.json > json-server.log 2>&1 &
```

### Rotating the database

To reset cleanly (e.g. after a destructive test run):

```bash
kill $(cat json-server.pid)
cp seeds/initial.json db.json
nohup npx json-server db.json > json-server.log 2>&1 &
```

### Monitoring

A few signals to watch if you must:

| Signal | What it tells you | How |
|--------|-------------------|-----|
| Process alive | Up / down | PID file, systemd, supervisor, etc. |
| Port responsive | Listener responding | `nc -z localhost 3000` or `curl -fs /` |
| `db.json` size | Steady-state vs. runaway growth | `stat db.json` or `du -h db.json` over time |
| Process RSS | Memory pressure | `ps -o rss= -p $PID` |
| Log file size | Background-process noise | Rotate `json-server.log` with `logrotate`. |

> [!NOTE]
> There are no metrics endpoints. No `/metrics`, no Prometheus exposition. Use process-level supervision tools (`systemd`, `supervisord`, `pm2`) for liveness; instrument at the proxy layer for request metrics.

### Updating the package

```bash
npm view json-server version          # check latest
npm install json-server@latest        # local install
npm install -g json-server@latest     # global install
```

The `1.0.0-beta.x` line is in active development. Pin a specific beta version (`json-server@1.0.0-beta.15`) in critical projects to avoid surprises.

---

## 6. Performance

Because every concern is in-process and small, performance is straightforward — and so are its limits.

### Latency profile

For a typical small dataset (hundreds to thousands of items), every operation is dominated by JavaScript dictionary lookups and array operations, not I/O. Expected latencies on a modern dev machine:

| Operation | Cost dominator | Typical latency |
|-----------|----------------|-----------------|
| `GET /:name` (no filter) | Array traversal + JSON serialize | < 5 ms |
| `GET /:name?filter` | Linear scan, `matchesWhere` per item | O(n) — sub-ms for n<1k |
| `GET /:name?_sort=...` | `sort-on` (in-place merge sort) | O(n log n) |
| `GET /:name?_page=...` | Paginate (slice) — no extra cost | < 5 ms |
| `GET /:name/:id` | Linear `Array.find` by id | O(n) |
| `POST /:name` | `randomId()` + `push` + full disk write | dominated by disk write |
| `PUT /:name/:id` | `Array.find` + `splice` + full disk write | dominated by disk write |
| `PATCH /:name/:id` | merge + `splice` + full disk write | dominated by disk write |
| `DELETE /:name/:id` | `Array.find` + `splice` + foreign-key sweep + full disk write | dominated by disk write |

### Disk I/O

Every successful write rewrites the **entire** `db.json`. There are no partial writes, no append-only mode, no journaling. With a 1 MB DB, that's ~1 MB of disk I/O per `POST` — fine on SSD, noticeable on HDD.

If you anticipate heavy write churn during a test run:

- Keep `db.json` small (only the seed data your test needs).
- Run on `tmpfs` if your OS supports it: `cp db.json /dev/shm/db.json && npx json-server /dev/shm/db.json` (POSIX/Linux only).

### Memory

DB size in memory is approximately `JSON.parse(db.json).length` plus framework overhead (~30 MB resident at idle). A 10 MB JSON file decodes to a roughly proportional in-memory structure. The whole tree is held — there is no streaming or lazy-loading path.

### Filtering performance

`matchesWhere` walks the predicate tree depth-first per item. For a query of `k` predicates against `n` items, cost is `O(n * k)`. Operators all run as constant-time JavaScript comparisons except `contains`/`startsWith`/`endsWith` (which lowercase both operands per call — a hot allocation in tight loops over large strings).

### Concurrency

Node single-threaded event loop. Every request is processed in order on the main thread. Mutations (`POST`/`PUT`/`PATCH`/`DELETE`) `await db.write()`, which blocks the event loop on disk I/O until the write completes — so concurrent writes serialize through the file write. Reads issued during a write don't block the write but will see stale data until the write resolves and the next request is dispatched.

> [!NOTE]
> There is no read-write lock. The in-memory `db.data` object is mutated synchronously during a write (e.g. `items.splice` in `service.destroyById` runs before `db.write()`). A simultaneous `GET` arriving between the in-memory mutation and the disk flush observes the new state, even though the write hasn't completed on disk. This is acceptable for mocking; it's not transactional.

### Scaling cliffs

| Scale | Behavior |
|-------|----------|
| 100 items, 1 KB each | Imperceptible. |
| 10 K items, 1 KB each | Every list-without-filter starts to feel slow if serialized to JSON in full. Pagination hides this. |
| 100 K items | List-everything responses get heavy (10s of MB). Filters that don't reduce cardinality are slow. Write latency rises linearly with file size. |
| 1 M items | You should not be using `json-server`. |

---

## 7. Security

This is the most important section to read carefully. `json-server` is **not** secure by default and was never intended to be exposed to untrusted networks.

### What json-server is NOT for

- **Production traffic.** No SLAs, no clustering, no observability, no auth.
- **Public-internet endpoints.** Even read-only — the data file may contain anything you wouldn't share publicly.
- **Untrusted networks.** Any client that can reach the port can fully read and rewrite your DB.
- **Sensitive data.** No encryption at rest, no field-level access control, no audit trail.

### The default security posture (zero defenses)

| Concern | json-server's default behavior |
|---------|-------------------------------|
| Authentication | **None.** Every request is anonymous and accepted. |
| Authorization | **None.** All resources are equally open. |
| Rate limiting | **None.** A client can issue unbounded writes. |
| CSRF protection | **None.** No tokens, no `Origin` checks. |
| CORS | **Wide open.** `@tinyhttp/cors` is mounted with the `Access-Control-Request-Headers` request header reflected, defaulting to allow any `Origin`. |
| Input validation | **Minimal.** Only "is this body a JSON object?" is checked; the entire body is stored as-is. |
| Schema enforcement on writes | **None at runtime.** The `$schema` reference is for editor tooling only — it is not used to reject invalid bodies. |
| Output sanitization | **None.** Stored values are echoed back verbatim. |
| Path traversal | Mitigated by `sirv` (which resolves and locks paths). The `-s` flag resolves directories against `process.cwd()`. |
| File write surface | The DB file path is the one passed on the CLI. No directory traversal from request data. |

> [!WARNING]
> Mounting `json-server` on a public IP without a reverse-proxy hardening layer is equivalent to publishing your DB file with a public read/write API. There are no built-in mitigations. If you do this, assume the data will be read, modified, or destroyed.

### The threat model that's safe

You're safe if **all** of the following hold:

- Bound to `localhost` (the default).
- Not started inside a process that can be reached from a hostile colocated tenant on the same machine.
- The DB file contains only fixture/test data.
- Started, used, and stopped within a single development session.

### Hardening recommendations (when you must run it on more than localhost)

| Layer | Approach |
|-------|----------|
| Network | Firewall the port to your office IP / VPN range. Bind to a non-default port to reduce drive-by scanner hits. |
| Reverse proxy | Front with Caddy/nginx/Traefik. Add basic auth, IP allowlists, mTLS, or OIDC at the proxy layer. Terminate TLS there. |
| Auth | Implement at the proxy. There is no in-process middleware mechanism in `1.0.0-beta.15` (issue [#1617](https://github.com/typicode/json-server/issues/1617)). |
| Rate limiting | At the proxy. Strict per-IP limits — every write rewrites your file. |
| CORS | Override at the proxy if you need a stricter `Access-Control-Allow-Origin` than the default. The bundled CORS middleware is permissive. |
| Backups | Frequent file snapshots. A single bad PATCH can shape-shift a record permanently. |
| Process isolation | Run as an unprivileged user. Use `systemd` `DynamicUser=yes`, AppArmor, or SELinux contexts to limit what the process can touch. |
| Capability | The process needs read/write on the DB file and read on any `-s` directories. Nothing else. |

### Embedding into a larger app for in-process auth

The cleanest way to add auth is to mount `createApp` inside a parent server you control:

```typescript
import { App } from '@tinyhttp/app'
import { createApp } from 'json-server/lib/app.js'

const parent = new App()

// Custom auth middleware
parent.use((req, res, next) => {
  if (req.headers.authorization !== `Bearer ${process.env.SECRET}`) {
    return res.status(401).json({ error: 'Unauthorized' })
  }
  next()
})

// Mount json-server's app under /api
const jsonServerApp = createApp(db)
parent.use('/api', jsonServerApp)

parent.listen(3000)
```

This pattern lets you keep `json-server`'s data semantics while owning the surrounding HTTP concerns.

### Known security-relevant issues

| Issue | Status | Impact | Mitigation |
|-------|--------|--------|------------|
| [#1731](https://github.com/typicode/json-server/issues/1731) Fail-open nested `_where` filtering | OPEN | A malformed nested predicate can cause filters to silently match more than intended. | Validate `_where` shape on the client before sending; never trust `json-server` to enforce filter intent on potentially-attacker-controlled queries. |
| [#1617](https://github.com/typicode/json-server/issues/1617) Middleware option missing | OPEN | No in-process hook for auth/validation/transformation. | Wrap with a parent app (above) or a reverse proxy. |

> [!CAUTION]
> No CVEs are tracked against `json-server` `1.0.0-beta.15` at the time of this commit. Absence of a CVE is **not** a security claim — the package does not advertise security guarantees and is not a hardened component. Treat all production-shaped deployments as defense-in-depth exercises with this layer assumed compromised.

### Defaults that are NOT security risks (but look like they should be)

- The auto-injected `$schema` pointer is a relative path (`./node_modules/json-server/schema.json`). Editors with JSON Schema support resolve it locally; it is not fetched at runtime by `json-server` itself.
- The base64url ID generator uses `crypto.randomBytes(8)`, a CSPRNG. The entropy is 64 bits — fine for opaque resource IDs in a non-adversarial environment, not strong enough for security tokens.
- File watching uses `chokidar` (no shell, no exec). External edits to `db.json` are read with `JSON.parse` / `JSON5.parse` — a malformed file produces a `SyntaxError` printed in red, not a crash.

---

## 8. Developer Internals

Everything an extender or contributor needs.

### Repository layout

```text
json-server/
├── src/                           Source code (TypeScript)
│   ├── bin.ts                     CLI entry point. Imports createApp, owns the watch loop.
│   ├── app.ts                     createApp(db, options): builds the @tinyhttp app.
│   ├── service.ts                 class Service: pure data ops on lowdb.
│   ├── matches-where.ts           matchesWhere(item, where): predicate evaluator.
│   ├── parse-where.ts             parseWhere(query): query string -> predicate tree.
│   ├── where-operators.ts         WHERE_OPERATORS constant + isWhereOperator type guard.
│   ├── paginate.ts                paginate(items, page, perPage): page wrapper.
│   ├── random-id.ts               randomId(): base64url 8-byte id.
│   ├── adapters/
│   │   ├── normalized-adapter.ts  NormalizedAdapter: $schema + id normalization.
│   │   ├── observer.ts            Observer: read/write hook adapter.
│   │   └── normalized-adapter.test.ts
│   ├── *.test.ts                  Per-module tests, run via node:test.
├── views/
│   └── index.html                 Eta template rendered at GET /.
├── public/                        Optional static dir (auto-served if it exists).
├── fixtures/
│   ├── db.json                    Sample fixture for `npm run dev`.
│   └── db.json5                   Sample JSON5 fixture.
├── schema.json                    JSON Schema for db.json shape.
├── tsconfig.json                  TS compiler config.
├── .oxfmtrc.json                  oxfmt config.
├── package.json                   Package manifest.
└── pnpm-lock.yaml                 Resolved dependency tree.
```

### Module-by-module reference

#### `src/bin.ts`

The CLI entry point. Does five things in sequence:

1. `args()` — parse `process.argv` with `node:util.parseArgs`. Handles `--version` (prints from `package.json`), `--help`, deprecated `--watch` (logs a warning), and the unknown-option error path. Returns `{file, port, host, static}`.
2. Validate the file path exists. If not, print red error and `process.exit(1)`. If empty, write `{}` into it.
3. Build the adapter chain: choose `JSONFile` or `DataFile` based on `extname(file)`, wrap with `NormalizedAdapter`, wrap with `Observer`. Construct `Low<Data>(observer, {})` and call `await db.read()`.
4. Call `createApp(db, {logger: false, static: staticArr})` and `app.listen(port, callback)`. The callback prints the colored startup banner with one of four kaomojis chosen at random.
5. If `NODE_ENV !== 'production'`: wire the Observer's `onReadStart/onReadEnd/onWriteStart/onWriteEnd` hooks and call `chokidar.watch(file).on('change', ...)`. The hooks coordinate so that the in-process write doesn't trigger its own re-read. On a real external change, `db.read()` is awaited; on a parse error the `SyntaxError` is printed in red.

#### `src/app.ts`

Exports `createApp(db, options)`. Order of mounting:

1. `sirv('public', {dev: !isProduction})` — auto-mount the `./public` directory.
2. For each path in `options.static`: resolve to absolute (against `process.cwd()` if not absolute) and `app.use(sirv(dir, {dev: !isProduction}))`.
3. CORS — installs `@tinyhttp/cors` as a per-request middleware that reflects the `Access-Control-Request-Headers` header. Also `app.options('*', cors())` for preflight.
4. `milliparsec.json()` — JSON body parser.
5. `GET /` — Eta render of `views/index.html` with `{data: db.data}`.
6. List/show/create/update/patch/delete handlers (see route table in Section 1).
7. Final `app.use('/:name', ...)` — sends `res.locals['data']` as JSON. `404 Not Found` if the data is undefined; `201` if the request was `POST`; `200` otherwise.

The two helpers `withBody` and `withIdAndBody` wrap a `Service` method into a middleware that validates `isItem(req.body)` and returns `400 Body must be a JSON object` if it fails. Otherwise it stashes the result on `res.locals['data']` and calls `next()`.

`parseListParams(req)` is the only non-trivial function here. It splits the query string by `RESERVED_QUERY_KEYS`, runs `parseWhere` on the remainder, and overrides with a parsed `_where` JSON if present and valid. Returns `{where, sort, page, perPage, embed}`.

#### `src/service.ts`

Class `Service` encapsulates all data semantics. Constructor takes a `Low<Data>`.

| Method | Signature | Behavior |
|--------|-----------|----------|
| `has(name)` | `(name: string) => boolean` | True iff `name` is a key in `db.data`. |
| `findById(name, id, query)` | `(name, id, {_embed?}) => Item \| undefined` | Linear `Array.find` by id. Applies each `_embed` relation. |
| `find(name, opts)` | `(name, {where, sort?, page?, perPage?, embed?}) => Item[] \| PaginatedItems \| Item \| undefined` | Embed → `matchesWhere` → optional `sort-on` → optional `paginate(items, page, perPage ?? 10)`. If the resource is a singleton (non-array), returns it directly. |
| `create(name, data)` | `(name, body) => Promise<Item \| undefined>` | Append `{...data, id: randomId()}` to the array. Calls `db.write()`. Returns the new item. |
| `update(name, body)` | `(name, body) => Promise<Item \| undefined>` | Replace a singleton. Returns `undefined` for arrays/unknown names. |
| `patch(name, body)` | `(name, body) => Promise<Item \| undefined>` | Merge into a singleton. |
| `updateById(name, id, body)` | `(name, id, body) => Promise<Item \| undefined>` | Replace an array item, preserving its `id`. Splice in place. |
| `patchById(name, id, body)` | `(name, id, body) => Promise<Item \| undefined>` | Merge fields into an array item by id. |
| `destroyById(name, id, dependent?)` | `(name, id, string \| string[]?) => Promise<Item \| undefined>` | Splice the item out. Run `nullifyForeignKey` across all other resources. Run `deleteDependents` for each named dependent. Calls `db.write()`. Returns the removed item. |

Two pure helpers in the same file:

- `embed(db, name, item, related)` — uses `inflection.singularize`/`pluralize` to decide direction and the foreign-key field name (`<singular>Id`).
- `nullifyForeignKey` / `deleteDependents` — sweep `db.data` for items with the matching FK.

#### `src/matches-where.ts`

Pure recursive evaluator. Public function `matchesWhere(obj, where) -> boolean`. The `or` key is special: it expects an array, and any sub-where matching the object is enough. For each non-`or` key:

- If value is a JSON object **and** contains any known operator, the field is checked against each operator. If the field is `undefined`, returns `false`.
- If value is a JSON object **without** known operators, recurse into a nested object on the same key. (This is the path that flags issue [#1731](https://github.com/typicode/json-server/issues/1731): a malformed sub-predicate falls through to nested traversal.)
- Otherwise, the predicate is treated as not-yet-supported and returns `false`.

#### `src/parse-where.ts`

Public function `parseWhere(queryString) -> JsonObject`. Internally:

- `splitKey(rawKey)` — strips the operator suffix. First tries colon (`field:op=value`), then underscore-style (`field_op=value`) for `0.17` compat, then defaults to `eq`.
- `coerceValue(rawValue)` — booleans/null/numbers/string fallback.
- `setPathOp(out, path, op, value)` — uses `dot-prop`'s `setProperty` to build nested objects from dotted paths. The `in` operator splits the value on `,` and coerces each part.

#### `src/paginate.ts`

Pure function. Always returns the full `PaginationResult<T>` shape. Inputs are clamped:

- `perPage`: only positive finite integers; otherwise `1`.
- `page`: clamped to `[1, pages]`.
- `pages = Math.max(1, Math.ceil(items.length / perPage))`.

`first` is always `1`. `last` is always `pages`. `prev`/`next` are `null` at the boundaries.

#### `src/random-id.ts`

```typescript
export function randomId(): string {
  return randomBytes(8).toString('base64url')
}
```

8 bytes → 11 base64url chars.

#### `src/where-operators.ts`

```typescript
export const WHERE_OPERATORS = [
  'lt','lte','gt','gte','eq','ne',
  'in','contains','startsWith','endsWith',
] as const
export type WhereOperator = (typeof WHERE_OPERATORS)[number]
export function isWhereOperator(value: string): value is WhereOperator {
  return (WHERE_OPERATORS as readonly string[]).includes(value)
}
```

The single source of truth for operator names. Adding an operator is a 4-step change: append here, handle in `matchesWhere`, document in the User Guide and this section, add tests in `matches-where.test.ts` and `parse-where.test.ts`.

#### `src/adapters/normalized-adapter.ts`

```typescript
export const DEFAULT_SCHEMA_PATH = './node_modules/json-server/schema.json'

export class NormalizedAdapter implements Adapter<Data> {
  // read():
  //   1. delegate to underlying adapter
  //   2. delete data['$schema']
  //   3. for each array value: coerce numeric `id` to string,
  //      generate randomId() for any item missing `id`
  //
  // write():
  //   1. delegate to underlying adapter with data + {$schema: DEFAULT_SCHEMA_PATH}
}
```

This is the integrity layer. ID stability and editor tooling both flow from here.

#### `src/adapters/observer.ts`

A pass-through adapter that fires `onReadStart`, `onReadEnd(data)`, `onWriteStart`, `onWriteEnd` hooks. Used in `bin.ts` to suppress the chokidar `change` event triggered by the app's own writes (the watcher checks `writing` flag set by `onWriteStart`/`onWriteEnd`).

### Code patterns and conventions

- **Pure helpers, mutable Service.** All data-shape functions (`paginate`, `matchesWhere`, `parseWhere`) are pure. Mutation is concentrated in the `Service` class and the `bin.ts` watcher.
- **Strict private fields.** `Service` uses `#db`. Adapters use `#adapter` / `#data`. Public surface is the `Adapter<T>` interface methods.
- **No exceptions for control flow.** Failures (unknown resource, missing id) return `undefined`; the route layer translates `undefined` into `404`.
- **Type-narrowing guards.** `isItem`, `isJSONObject`, `isWhereOperator` are explicit type guards used to keep the rest of the code well-typed.
- **Style.** No semicolons (`oxfmt` config). Single quotes. Strict-types-on `tsconfig`.

### Testing strategy

The test runner is Node's built-in `node:test`. The `test` npm script:

```bash
npm test
# expands to:
node --experimental-strip-types --test src/*.test.ts
```

Tests run against the TypeScript source directly (no separate build step) thanks to `--experimental-strip-types` in Node 22.

| Test file | Coverage |
|-----------|----------|
| `src/app.test.ts` | End-to-end: spins up the app on a free port, exercises every method × every route shape, checks status codes and error bodies. |
| `src/service.test.ts` | Per-method service contract: `findById`, `find`, `create` (id generation/forgery resistance), `update`, `patch`, `updateById`, `patchById`, `destroyById` (FK nullify and cascade). |
| `src/matches-where.test.ts` | Operator semantics for every `WHERE_OPERATOR` plus nested fields, `or`, unknown ops, type-mismatch behavior. |
| `src/parse-where.test.ts` | Query-string parsing including v0.17 underscore syntax, dotted paths, `in` list parsing, type coercion. |
| `src/paginate.test.ts` | Boundary cases: clamp high/low pages, `_per_page=0`, empty input. |
| `src/adapters/normalized-adapter.test.ts` | `$schema` strip on read / inject on write, numeric-id coercion, random-id generation for missing ids. |

Adding a feature should add tests in the matching file. The existing tests are good models — they're table-driven (`for (const tc of cases) ...`) and assert with `node:assert/strict`.

### Build and publish

```bash
npm run typecheck      # tsc --noEmit
npm run build          # rm -rf lib && tsc           (compiles src -> lib)
npm run prepublishOnly # rm -rf lib && tsc           (auto-runs before npm publish)
```

Published package contents (per `files` in `package.json`):

```text
lib/        compiled JavaScript
views/      Eta templates
schema.json the JSON Schema
```

The published `bin` field is `lib/bin.js` (post-compile path). At runtime, when `dist`-style features rely on file paths (e.g. `views/`), those are resolved via `import.meta.url` — see `eta = new Eta({views: join(__dirname, '../views')})` in `src/app.ts:21`. This means the runtime expects `lib/` and `views/` to be siblings, which the `files` array enforces.

### Linting and formatting

```bash
npm run lint           # oxlint src
npm run fmt            # oxfmt
npm run fmt:check      # oxfmt --check
```

`oxlint` and `oxfmt` are Rust-based — fast, no Node-side overhead in CI. The `prepare` script wires up `husky` for pre-commit hooks.

### Potential improvements

Worth considering for future betas (none of these are in `1.0.0-beta.15`):

- **Re-introduce `--delay`** ([#1545](https://github.com/typicode/json-server/issues/1545), [#1558](https://github.com/typicode/json-server/issues/1558)). Simulating latency is the most-requested missing feature.
- **Middleware hook** ([#1617](https://github.com/typicode/json-server/issues/1617)). A documented place to register pre-route auth/validation logic, even if it's just exposing the underlying `App` instance.
- **`X-Total-Count` header** ([#1727](https://github.com/typicode/json-server/issues/1727)). Easy parity with the older API and many third-party admin UIs that look for it.
- **Custom routes / route prefix** — even just an option to mount the API under `/api` would close [#1710](https://github.com/typicode/json-server/issues/1710)-class issues without requiring a reverse proxy.
- **Strict operator validation** ([#1731](https://github.com/typicode/json-server/issues/1731)). Reject unknown operators inside nested `_where` rather than silently descending.
- **Configurable embed key suffix** ([#1634](https://github.com/typicode/json-server/issues/1634)). Allow `post_id` / `parent` instead of forcing `xxxId`.
- **Custom POST response shape** ([#1619](https://github.com/typicode/json-server/issues/1619)). Pluggable response transformation.
- **File-upload support** ([#528](https://github.com/typicode/json-server/issues/528)). Long-standing request; would need either a separate route family or a switch to `multipart/form-data` parsing.

### Contributing

Quick contributor loop:

```bash
git clone https://github.com/typicode/json-server.git
cd json-server
npm install
npm run dev -- fixtures/db.json   # live-reload TypeScript runner
# edit src/...
npm test                          # run the suite
npm run lint
npm run fmt
git commit -m "..."               # husky runs pre-commit hooks
```

Tests live next to the source file they cover (`src/foo.ts` ↔ `src/foo.test.ts`). PRs should keep that pattern.

> [!TIP]
> When changing `WHERE_OPERATORS`, be sure to update both the matcher (`matches-where.ts`) and the parser (`parse-where.ts`) and add table-driven tests to **both** of their `.test.ts` files. Adding the constant alone doesn't expose the operator at runtime.

---

## 9. Integration Points

How `json-server` slots into other tools and stacks.

### Frontend frameworks

The integration is universal: `json-server` is just an HTTP server. Point your fetch base URL at it and you're done.

| Stack | Pattern |
|-------|---------|
| Vite (React/Vue/Svelte/Solid) | Run on a different port; configure Vite's `server.proxy` to forward `/api` to `http://localhost:3000`. |
| Next.js | Run alongside, use as the data source for `getServerSideProps` or fetched from client components. |
| Create React App | Set `"proxy": "http://localhost:3000"` in `package.json`. |
| Plain HTML + fetch | Same-origin if you serve your HTML via `-s`, otherwise rely on the open CORS. |

### Test runners

Run as a backgrounded process during the suite. See "CI / automated tests" in Section 4. The test fixtures themselves live in `fixtures/db.json` in the project; copy that pattern to seed your own.

### Programmatic embedding

`createApp` is exported from `src/app.ts`. The published path is `json-server/lib/app.js` after `npm run build`. A minimal embed:

```typescript
import { Low, Memory } from 'lowdb'
import { createApp } from 'json-server/lib/app.js'

const db = new Low(new Memory(), {
  posts: [{ id: '1', title: 'Hello' }],
})

const app = createApp(db, { static: [] })
app.listen(3000)
```

For real-file persistence:

```typescript
import { Low } from 'lowdb'
import { JSONFile } from 'lowdb/node'
import { createApp } from 'json-server/lib/app.js'
import { NormalizedAdapter } from 'json-server/lib/adapters/normalized-adapter.js'

const adapter = new NormalizedAdapter(new JSONFile('./db.json'))
const db = new Low(adapter, {})
await db.read()

const app = createApp(db)
app.listen(3000)
```

> [!IMPORTANT]
> When embedding, match the adapter chain that `bin.ts` constructs (`NormalizedAdapter` wrapping the file adapter). Skipping `NormalizedAdapter` means missing IDs won't be auto-generated and numeric IDs won't be coerced — every other layer assumes string IDs.

### lowdb adapters

Because `json-server` accepts any `Low<Data>`, you can swap in any `lowdb` adapter:

| Adapter | Use case |
|---------|----------|
| `Memory` | In-memory only; data is lost on restart. Test setups. |
| `JSONFile` (default for `.json`) | Single file on disk. |
| `DataFile` (used for `.json5`) | Parser/stringifier injection — also useful for YAML or custom serializers. |

The `Adapter<T>` interface is `{read(): Promise<T \| null>, write(data: T): Promise<void>}`. Anything matching that contract works.

### Reverse proxies

See Section 4 Deployment for Caddy, nginx, and Vite proxy snippets. The key constraint is that `json-server` doesn't honor `X-Forwarded-*` headers natively — if you want client IPs in your access logs, capture them at the proxy layer.

### Observability / Logging tools

There is no integration with structured logging libraries (Pino, Winston) out of the box. Two reasonable patches:

1. **At the proxy layer.** Caddy/nginx access logs are usually enough for development.
2. **Inside the embedded app** (when using `createApp`):

   ```typescript
   const app = createApp(db)
   app.use((req, _res, next) => {
     console.log(`${new Date().toISOString()} ${req.method} ${req.url}`)
     next()
   })
   app.listen(3000)
   ```

### CI/CD platforms

`json-server` is happy to run on any worker that has Node 22. No native deps means no platform-specific tweaks. Long-lived test workers can install globally; ephemeral (GitHub Actions, GitLab CI) prefer `npx` per run.

```yaml
# .github/workflows/test.yml (example)
- uses: actions/setup-node@v4
  with:
    node-version: '22'
- run: |
    npx json-server -p 4000 fixtures/db.json &
    npm test
```

### What does NOT integrate

For clarity:

- **No webhook or event emission.** Mutations don't notify any external system.
- **No streaming / WebSockets / SSE.** All endpoints are request/response.
- **No GraphQL.** REST only.
- **No authentication providers (OAuth, OIDC, JWT verification).** Auth is up to you, at the edge.
- **No queue or pub/sub adapter.** State changes are local-only.

> [!NOTE]
> `json-server`'s strength is minimalism. Each integration above achieves its goal by leaving `json-server` alone and adding capability around it. The package itself is intentionally not a platform.

---

_Generated April 2026 from typicode/json-server at 89a34a4. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
