Full Code of typicode/json-server for AI

main 352144e0f6c8 cached
32 files
59.9 KB
17.9k tokens
68 symbols
2 requests
Download .txt
Repository: typicode/json-server
Branch: main
Commit: 352144e0f6c8
Files: 32
Total size: 59.9 KB

Directory structure:
gitextract_grojiw9z/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── node.js.yml
│       └── publish.yml
├── .gitignore
├── .husky/
│   └── pre-commit
├── .oxfmtrc.json
├── LICENSE
├── README.md
├── fixtures/
│   ├── db.json
│   └── db.json5
├── package.json
├── public/
│   └── test.html
├── schema.json
├── src/
│   ├── adapters/
│   │   ├── normalized-adapter.test.ts
│   │   ├── normalized-adapter.ts
│   │   └── observer.ts
│   ├── app.test.ts
│   ├── app.ts
│   ├── bin.ts
│   ├── matches-where.test.ts
│   ├── matches-where.ts
│   ├── paginate.test.ts
│   ├── paginate.ts
│   ├── parse-where.test.ts
│   ├── parse-where.ts
│   ├── random-id.ts
│   ├── service.test.ts
│   ├── service.ts
│   └── where-operators.ts
├── tsconfig.json
└── views/
    └── index.html

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
*.ts linguist-language=JavaScript

================================================
FILE: .github/FUNDING.yml
================================================
github: typicode


================================================
FILE: .github/workflows/node.js.yml
================================================
name: Node.js CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v4
        with:
          version: 10
      - uses: actions/setup-node@v6
        with:
          node-version: "22.x"
          cache: "pnpm"
      - run: pnpm install
      - run: pnpm run lint
      - run: pnpm run typecheck
      - run: pnpm test


================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish Package to npmjs
on:
  release:
    types: [published]
permissions:
  id-token: write  # Required for OIDC
  contents: read
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v4
        with:
          version: 10
      - uses: actions/setup-node@v6
        with:
          node-version: "24.x"
          registry-url: "https://registry.npmjs.org"
      - run: pnpm install
      - run: pnpm publish --provenance --access public --no-git-checks --tag latest


================================================
FILE: .gitignore
================================================
**/*.log
.DS_Store
.idea
lib
node_modules
public/output.css
tmp


================================================
FILE: .husky/pre-commit
================================================
pnpm test


================================================
FILE: .oxfmtrc.json
================================================
{
  "semi": false,
  "singleQuote": true,
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2026 typicode

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# JSON-Server

[![Node.js CI](https://github.com/typicode/json-server/actions/workflows/node.js.yml/badge.svg)](https://github.com/typicode/json-server/actions/workflows/node.js.yml)

> [!IMPORTANT]
> Viewing beta v1 documentation – usable but expect breaking changes. For stable version, see [here](https://github.com/typicode/json-server/tree/v0.17.4)

> [!NOTE]
> Using React ⚛️ and tired of CSS-in-JS? See [MistCSS](https://github.com/typicode/mistcss) 👀

## Install

```shell
npm install json-server
```

## Usage

Create a `db.json` or `db.json5` file

```json
{
  "$schema": "./node_modules/json-server/schema.json",
  "posts": [
    { "id": "1", "title": "a title", "views": 100 },
    { "id": "2", "title": "another title", "views": 200 }
  ],
  "comments": [
    { "id": "1", "text": "a comment about post 1", "postId": "1" },
    { "id": "2", "text": "another comment about post 1", "postId": "1" }
  ],
  "profile": {
    "name": "typicode"
  }
}
```

<details>

<summary>View db.json5 example</summary>

```json5
{
  posts: [
    { id: "1", title: "a title", views: 100 },
    { id: "2", title: "another title", views: 200 },
  ],
  comments: [
    { id: "1", text: "a comment about post 1", postId: "1" },
    { id: "2", text: "another comment about post 1", postId: "1" },
  ],
  profile: {
    name: "typicode",
  },
}
```

You can read more about JSON5 format [here](https://github.com/json5/json5).

</details>

Start JSON Server

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

This starts the server at `http://localhost:3000`. You should see:
```
JSON Server started on PORT :3000
http://localhost:3000
```

Access your REST API:

```bash
curl http://localhost:3000/posts/1
```

**Response:**
```json
{
  "id": "1",
  "title": "a title",
  "views": 100
}
```

Run `json-server --help` for a list of options

## Sponsors ✨

### Gold

|                                                                                                                                                            |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------: |
|               <a href="https://mockend.com/" target="_blank"><img src="https://jsonplaceholder.typicode.com/mockend.svg" height="100px"></a>               |
| <a href="https://zuplo.link/json-server-gh"><img src="https://github.com/user-attachments/assets/adfee31f-a8b6-4684-9a9b-af4f03ac5b75" height="100px"></a> |
|     <a href="https://www.mintlify.com/"><img src="https://github.com/user-attachments/assets/bcc8cc48-b2d9-4577-8939-1eb4196b7cc5" height="100px"></a>     |
| <a href="http://git-tower.com/?utm_source=husky&utm_medium=referral"><img height="100px" alt="tower-dock-icon-light" src="https://jsonplaceholder.typicode.com/tower-icon-and-logo-1400x260.png" /></a> |
| <a href="https://serpapi.com/?utm_source=typicode"><img height="100px" src="https://github.com/user-attachments/assets/52b3039d-1e4c-4c68-951c-93f0f1e73611" /></a>


### Silver

|                                                                                                                                                                                                                                         |
| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| <a href="https://requestly.com?utm_source=githubsponsor&utm_medium=jsonserver&utm_campaign=jsonserver"><img src="https://github.com/user-attachments/assets/f7e7b3cf-97e2-46b8-81c8-cb3992662a1c" style="height:70px; width:auto;"></a> |

### Bronze

|                                                                                                                                                                                |                                                                                                                                                                              |
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| <a href="https://www.storyblok.com/" target="_blank"><img src="https://github.com/typicode/json-server/assets/5502029/c6b10674-4ada-4616-91b8-59d30046b45a" height="35px"></a> | <a href="https://betterstack.com/" target="_blank"><img src="https://github.com/typicode/json-server/assets/5502029/44679f8f-9671-470d-b77e-26d90b90cbdc" height="35px"></a> |

[Become a sponsor and have your company logo here](https://github.com/users/typicode/sponsorship)

## Query Capabilities

JSON Server supports advanced querying out of the box:

```http
GET /posts?views:gt=100                  # Filter by condition
GET /posts?_sort=-views                  # Sort by field (descending)
GET /posts?_page=1&_per_page=10          # Pagination
GET /posts?_embed=comments               # Include relations
GET /posts?_where={"or":[...]}           # Complex queries
```

See detailed documentation below for each feature.

## Routes

### Array Resources

For array resources like `posts` and `comments`:

```http
GET    /posts
GET    /posts/:id
POST   /posts
PUT    /posts/:id
PATCH  /posts/:id
DELETE /posts/:id
```

### Object Resources

For singular object resources like `profile`:

```http
GET   /profile
PUT   /profile
PATCH /profile
```

## Query params

### Conditions

Use `field:operator=value`.

Operators:

- no operator -> `eq` (equal)
- `lt` less than, `lte` less than or equal
- `gt` greater than, `gte` greater than or equal
- `eq` equal, `ne` not equal
- `in` included in comma-separated list
- `contains` string contains (case-insensitive)
- `startsWith` string starts with (case-insensitive)
- `endsWith` string ends with (case-insensitive)

Examples:

```http
GET /posts?views:gt=100
GET /posts?title:eq=Hello
GET /posts?id:in=1,2,3
GET /posts?author.name:eq=typicode
GET /posts?title:contains=hello
GET /posts?title:startsWith=Hello
GET /posts?title:endsWith=world
```

### Sort

```http
GET /posts?_sort=title
GET /posts?_sort=-views
GET /posts?_sort=author.name,-views
```

### Pagination

```http
GET /posts?_page=1&_per_page=25
```

**Response:**
```json
{
  "first": 1,
  "prev": null,
  "next": 2,
  "last": 4,
  "pages": 4,
  "items": 100,
  "data": [
    { "id": "1", "title": "...", "views": 100 },
    { "id": "2", "title": "...", "views": 200 }
  ]
}
```

**Notes:**
- `_per_page` defaults to `10` if not specified
- Invalid `_page` or `_per_page` values are automatically normalized to valid ranges

### Embed

```http
GET /posts?_embed=comments
GET /comments?_embed=post
```

### Complex filter with `_where`

`_where` accepts a JSON object and overrides normal query params when valid.

```http
GET /posts?_where={"or":[{"views":{"gt":100}},{"author":{"name":{"lt":"m"}}}]}
```

## Delete dependents

```http
DELETE /posts/1?_dependent=comments
```

## Static Files

JSON Server automatically serves files from the `./public` directory.

To serve additional static directories:

```bash
json-server db.json -s ./static
json-server db.json -s ./static -s ./node_modules
```

Static files are served with standard MIME types and can include HTML, CSS, JavaScript, images, and other assets.

## Migration Notes (v0 → v1)

If you are upgrading from json-server v0.x, note these behavioral changes:

- **ID handling:** `id` is always a string and will be auto-generated if not provided
- **Pagination:** Use `_per_page` with `_page` instead of the deprecated `_limit` parameter
- **Relationships:** Use `_embed` instead of `_expand` for including related resources
- **Request delays:** Use browser DevTools (Network tab > throttling) instead of the removed `--delay` CLI option

> **New to json-server?** These notes are for users migrating from v0. If this is your first time using json-server, you can ignore this section.


================================================
FILE: fixtures/db.json
================================================
{
  "posts": [
    { "id": "1", "title": "a title" },
    { "id": "2", "title": "another title" }
  ],
  "comments": [
    { "id": "1", "text": "a comment about post 1", "postId": "1" },
    { "id": "2", "text": "another comment about post 1", "postId": "1" }
  ],
  "profile": {
    "name": "typicode"
  }
}


================================================
FILE: fixtures/db.json5
================================================
{
  posts: [
    {
      id: "1",
      title: "a title",
    },
    {
      id: "2",
      title: "another title",
    },
  ],
  comments: [
    {
      id: "1",
      text: "a comment about post 1",
      postId: "1",
    },
    {
      id: "2",
      text: "another comment about post 1",
      postId: "1",
    },
  ],
  profile: {
    name: "typicode",
  },
}


================================================
FILE: package.json
================================================
{
  "name": "json-server",
  "version": "1.0.0-beta.13",
  "description": "",
  "keywords": [
    "JSON",
    "server",
    "fake",
    "REST",
    "API",
    "prototyping",
    "mock",
    "mocking",
    "test",
    "testing",
    "rest",
    "data",
    "dummy"
  ],
  "license": "MIT",
  "author": "typicode <typicode@gmail.com>",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/typicode/json-server.git"
  },
  "bin": {
    "json-server": "lib/bin.js"
  },
  "files": [
    "lib",
    "views",
    "schema.json"
  ],
  "type": "module",
  "scripts": {
    "dev": "node --watch --experimental-strip-types src/bin.ts fixtures/db.json",
    "build": "rm -rf lib && tsc",
    "prepublishOnly": "rm -rf lib && tsc",
    "typecheck": "tsc --noEmit",
    "test": "node --experimental-strip-types --test src/*.test.ts",
    "lint": "oxlint src",
    "fmt": "oxfmt",
    "fmt:check": "oxfmt --check",
    "prepare": "husky"
  },
  "dependencies": {
    "@tinyhttp/app": "^3.0.1",
    "@tinyhttp/cors": "^2.0.1",
    "@tinyhttp/logger": "^2.1.0",
    "chalk": "^5.6.2",
    "chokidar": "^5.0.0",
    "dot-prop": "^10.1.0",
    "eta": "^4.5.0",
    "inflection": "^3.0.2",
    "json5": "^2.2.3",
    "lowdb": "^7.0.1",
    "milliparsec": "^5.1.0",
    "sirv": "^3.0.2",
    "sort-on": "^7.0.0"
  },
  "devDependencies": {
    "@types/node": "^25.0.8",
    "concurrently": "^9.2.1",
    "get-port": "^7.1.0",
    "husky": "^9.1.7",
    "oxfmt": "^0.24.0",
    "oxlint": "^1.39.0",
    "tempy": "^3.1.0",
    "type-fest": "^5.4.0",
    "typescript": "^5.9.3"
  },
  "engines": {
    "node": ">=22.12.0"
  }
}


================================================
FILE: public/test.html
================================================
<!-- Testing automatic serving of files from the 'public/' directory -->


================================================
FILE: schema.json
================================================
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": {
    "oneOf": [
      { "type": "array" },
      { "type": "object" }
    ]
  }
}


================================================
FILE: src/adapters/normalized-adapter.test.ts
================================================
import assert from 'node:assert/strict'
import test from 'node:test'

import type { Adapter } from 'lowdb'

import { DEFAULT_SCHEMA_PATH, NormalizedAdapter } from './adapters/normalized-adapter.ts'
import type { RawData } from './adapters/normalized-adapter.ts'
import type { Data } from './service.ts'

class StubAdapter implements Adapter<RawData> {
  #data: RawData | null

  constructor(data: RawData | null) {
    this.#data = data
  }

  async read(): Promise<RawData | null> {
    return this.#data === null ? null : structuredClone(this.#data)
  }

  async write(data: RawData): Promise<void> {
    this.#data = structuredClone(data)
  }

  get data(): RawData | null {
    return this.#data
  }
}

await test('read removes $schema and normalizes ids', async () => {
  const adapter = new StubAdapter({
    $schema: './custom/schema.json',
    posts: [{ id: 1 }, { title: 'missing id' }],
    profile: { name: 'x' },
  })

  const normalized = await new NormalizedAdapter(adapter).read()
  assert.notEqual(normalized, null)

  if (normalized === null) {
    return
  }

  assert.equal(normalized['$schema'], undefined)
  assert.deepEqual(normalized['profile'], { name: 'x' })

  const posts = normalized['posts']
  assert.ok(Array.isArray(posts))
  assert.equal(posts[0]?.['id'], '1')
  assert.equal(typeof posts[1]?.['id'], 'string')
  assert.notEqual(posts[1]?.['id'], '')
})

await test('write always overwrites $schema', async () => {
  const adapter = new StubAdapter(null)
  const normalizedAdapter = new NormalizedAdapter(adapter)

  await normalizedAdapter.write({ posts: [{ id: '1' }] } satisfies Data)

  const data = adapter.data
  assert.notEqual(data, null)
  assert.equal(data?.['$schema'], DEFAULT_SCHEMA_PATH)
})


================================================
FILE: src/adapters/normalized-adapter.ts
================================================
import type { Adapter } from 'lowdb'

import { randomId } from '../random-id.ts'
import type { Data, Item } from '../service.ts'

export const DEFAULT_SCHEMA_PATH = './node_modules/json-server/schema.json'
export type RawData = Record<string, Item[] | Item | string | undefined> & {
  $schema?: string
}

export class NormalizedAdapter implements Adapter<Data> {
  #adapter: Adapter<RawData>

  constructor(adapter: Adapter<RawData>) {
    this.#adapter = adapter
  }

  async read(): Promise<Data | null> {
    const data = await this.#adapter.read()

    if (data === null) {
      return null
    }

    delete data['$schema']

    for (const value of Object.values(data)) {
      if (Array.isArray(value)) {
        for (const item of value) {
          if (typeof item['id'] === 'number') {
            item['id'] = item['id'].toString()
          }

          if (item['id'] === undefined) {
            item['id'] = randomId()
          }
        }
      }
    }

    return data as Data
  }

  async write(data: Data): Promise<void> {
    await this.#adapter.write({ ...data, $schema: DEFAULT_SCHEMA_PATH })
  }
}


================================================
FILE: src/adapters/observer.ts
================================================
import type { Adapter } from 'lowdb'

// Lowdb adapter to observe read/write events
export class Observer<T> {
  #adapter: Adapter<T>

  onReadStart = function () {
    return
  }
  onReadEnd: (data: T | null) => void = function () {
    return
  }
  onWriteStart = function () {
    return
  }
  onWriteEnd = function () {
    return
  }

  constructor(adapter: Adapter<T>) {
    this.#adapter = adapter
  }

  async read() {
    this.onReadStart()
    const data = await this.#adapter.read()
    this.onReadEnd(data)
    return data
  }

  async write(arg: T) {
    this.onWriteStart()
    await this.#adapter.write(arg)
    this.onWriteEnd()
  }
}


================================================
FILE: src/app.test.ts
================================================
   import assert from 'node:assert/strict'
import { writeFileSync } from 'node:fs'
import { join } from 'node:path'
import test from 'node:test'

import getPort from 'get-port'
import { Low, Memory } from 'lowdb'
import { temporaryDirectory } from 'tempy'

import { createApp } from './app.ts'
import type { Data } from './service.ts'

type Test = {
  method: HTTPMethods
  url: string
  statusCode: number
}

type HTTPMethods = 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'OPTIONS'

const port = await getPort()

// Create custom static dir with an html file
const tmpDir = temporaryDirectory()
const file = 'file.html'
writeFileSync(join(tmpDir, file), 'utf-8')

// Create app
const db = new Low<Data>(new Memory<Data>(), {})
db.data = {
  posts: [{ id: '1', title: 'foo' }],
  comments: [{ id: '1', postId: '1' }],
  object: { f1: 'foo' },
}
const app = createApp(db, { static: [tmpDir] })

await new Promise<void>((resolve, reject) => {
  try {
    const server = app.listen(port, () => resolve())
    test.after(() => server.close())
  } catch (err) {
    reject(err)
  }
})

await test('createApp', async (t) => {
  // URLs
  const POSTS = '/posts'
  const POSTS_WITH_COMMENTS = '/posts?_embed=comments'
  const POST_1 = '/posts/1'
  const POST_NOT_FOUND = '/posts/-1'
  const POST_WITH_COMMENTS = '/posts/1?_embed=comments'
  const COMMENTS = '/comments'
  const POST_COMMENTS = '/comments?postId=1'
  const NOT_FOUND = '/not-found'
  const OBJECT = '/object'
  const OBJECT_1 = '/object/1'

  const arr: Test[] = [
    // Static
    { method: 'GET', url: '/', statusCode: 200 },
    { method: 'GET', url: '/test.html', statusCode: 200 },
    { method: 'GET', url: `/${file}`, statusCode: 200 },

    // CORS
    { method: 'OPTIONS', url: POSTS, statusCode: 204 },

    // API
    { method: 'GET', url: POSTS, statusCode: 200 },
    { method: 'GET', url: POSTS_WITH_COMMENTS, statusCode: 200 },
    { method: 'GET', url: POST_1, statusCode: 200 },
    { method: 'GET', url: POST_NOT_FOUND, statusCode: 404 },
    { method: 'GET', url: POST_WITH_COMMENTS, statusCode: 200 },
    { method: 'GET', url: COMMENTS, statusCode: 200 },
    { method: 'GET', url: POST_COMMENTS, statusCode: 200 },
    { method: 'GET', url: OBJECT, statusCode: 200 },
    { method: 'GET', url: OBJECT_1, statusCode: 404 },
    { method: 'GET', url: NOT_FOUND, statusCode: 404 },

    { method: 'POST', url: POSTS, statusCode: 201 },
    { method: 'POST', url: POST_1, statusCode: 404 },
    { method: 'POST', url: POST_NOT_FOUND, statusCode: 404 },
    { method: 'POST', url: OBJECT, statusCode: 404 },
    { method: 'POST', url: OBJECT_1, statusCode: 404 },
    { method: 'POST', url: NOT_FOUND, statusCode: 404 },

    { method: 'PUT', url: POSTS, statusCode: 404 },
    { method: 'PUT', url: POST_1, statusCode: 200 },
    { method: 'PUT', url: OBJECT, statusCode: 200 },
    { method: 'PUT', url: OBJECT_1, statusCode: 404 },
    { method: 'PUT', url: POST_NOT_FOUND, statusCode: 404 },
    { method: 'PUT', url: NOT_FOUND, statusCode: 404 },

    { method: 'PATCH', url: POSTS, statusCode: 404 },
    { method: 'PATCH', url: POST_1, statusCode: 200 },
    { method: 'PATCH', url: OBJECT, statusCode: 200 },
    { method: 'PATCH', url: OBJECT_1, statusCode: 404 },
    { method: 'PATCH', url: POST_NOT_FOUND, statusCode: 404 },
    { method: 'PATCH', url: NOT_FOUND, statusCode: 404 },

    { method: 'DELETE', url: POSTS, statusCode: 404 },
    { method: 'DELETE', url: POST_1, statusCode: 200 },
    { method: 'DELETE', url: OBJECT, statusCode: 404 },
    { method: 'DELETE', url: OBJECT_1, statusCode: 404 },
    { method: 'DELETE', url: POST_NOT_FOUND, statusCode: 404 },
    { method: 'DELETE', url: NOT_FOUND, statusCode: 404 },
  ]

  for (const tc of arr) {
    await t.test(`${tc.method} ${tc.url}`, async () => {
      const response = await fetch(`http://localhost:${port}${tc.url}`, {
        method: tc.method,
      })
      assert.equal(
        response.status,
        tc.statusCode,
        `${response.status} !== ${tc.statusCode} ${tc.method} ${tc.url} failed`,
      )
    })
  }

  await t.test('GET /posts?_where=... uses JSON query', async () => {
    // Reset data since previous tests may have modified it
    db.data = {
      posts: [{ id: '1', title: 'foo' }],
      comments: [{ id: '1', postId: '1' }],
      object: { f1: 'foo' },
    }
    const where = encodeURIComponent(JSON.stringify({ title: { eq: 'foo' } }))
    const response = await fetch(`http://localhost:${port}/posts?_where=${where}`)
    assert.equal(response.status, 200)
    const data = await response.json()
    assert.deepEqual(data, [{ id: '1', title: 'foo' }])
  })

  await t.test('GET /posts?_where=... overrides query params', async () => {
    const where = encodeURIComponent(JSON.stringify({ title: { eq: 'foo' } }))
    const response = await fetch(
      `http://localhost:${port}/posts?title:eq=bar&_where=${where}`,
    )
    assert.equal(response.status, 200)
    const data = await response.json()
    assert.deepEqual(data, [{ id: '1', title: 'foo' }])
  })

  await t.test('POST /posts with array body returns 400', async () => {
    const response = await fetch(`http://localhost:${port}/posts`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify([{ title: 'foo' }]),
    })
    assert.equal(response.status, 400)
    const data = await response.json()
    assert.deepEqual(data, { error: 'Body must be a JSON object' })
  })

  await t.test('PATCH /posts/1 with string body returns 400', async () => {
    const response = await fetch(`http://localhost:${port}/posts/1`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify('hello'),
    })
    assert.equal(response.status, 400)
    const data = await response.json()
    assert.deepEqual(data, { error: 'Body must be a JSON object' })
  })

  await t.test('PUT /posts/1 with null body returns 400', async () => {
    const response = await fetch(`http://localhost:${port}/posts/1`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(null),
    })
    assert.equal(response.status, 400)
    const data = await response.json()
    assert.deepEqual(data, { error: 'Body must be a JSON object' })
  })
})


================================================
FILE: src/app.ts
================================================
import { dirname, isAbsolute, join } from 'node:path'
import { fileURLToPath } from 'node:url'

import { App } from '@tinyhttp/app'
import { cors } from '@tinyhttp/cors'
import { Eta } from 'eta'
import { Low } from 'lowdb'
import { json } from 'milliparsec'
import sirv from 'sirv'

import { parseWhere } from './parse-where.ts'
import type { Data } from './service.ts'
import { isItem, Service } from './service.ts'

const __dirname = dirname(fileURLToPath(import.meta.url))
const isProduction = process.env['NODE_ENV'] === 'production'

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

const eta = new Eta({
  views: join(__dirname, '../views'),
  cache: isProduction,
})

const RESERVED_QUERY_KEYS = new Set(['_sort', '_page', '_per_page', '_embed', '_where'])

function parseListParams(req: any) {
  const queryString = req.url.split('?')[1] ?? ''
  const params = new URLSearchParams(queryString)

  const filterParams = new URLSearchParams()
  for (const [key, value] of params.entries()) {
    if (!RESERVED_QUERY_KEYS.has(key)) {
      filterParams.append(key, value)
    }
  }

  let where = parseWhere(filterParams.toString())
  const rawWhere = params.get('_where')
  if (typeof rawWhere === 'string') {
    try {
      const parsed = JSON.parse(rawWhere)
      if (typeof parsed === 'object' && parsed !== null) {
        where = parsed
      }
    } catch {
      // Ignore invalid JSON and fallback to parsed query params
    }
  }

  const pageRaw = params.get('_page')
  const perPageRaw = params.get('_per_page')
  const page = pageRaw === null ? undefined : Number.parseInt(pageRaw, 10)
  const perPage = perPageRaw === null ? undefined : Number.parseInt(perPageRaw, 10)

  return {
    where,
    sort: params.get('_sort') ?? undefined,
    page: Number.isNaN(page) ? undefined : page,
    perPage: Number.isNaN(perPage) ? undefined : perPage,
    embed: req.query['_embed'],
  }
}

function withBody(action: (name: string, body: Record<string, unknown>) => Promise<unknown>) {
  return async (req: any, res: any, next: any) => {
    const { name = '' } = req.params
    if (!isItem(req.body)) {
      res.status(400).json({ error: 'Body must be a JSON object' })
      return
    }
    res.locals['data'] = await action(name, req.body)
    next?.()
  }
}

function withIdAndBody(
  action: (name: string, id: string, body: Record<string, unknown>) => Promise<unknown>,
) {
  return async (req: any, res: any, next: any) => {
    const { name = '', id = '' } = req.params
    if (!isItem(req.body)) {
      res.status(400).json({ error: 'Body must be a JSON object' })
      return
    }
    res.locals['data'] = await action(name, id, req.body)
    next?.()
  }
}

export function createApp(db: Low<Data>, options: AppOptions = {}) {
  // Create service
  const service = new Service(db)

  // Create app
  const app = new App()

  // Static files
  app.use(sirv('public', { dev: !isProduction }))
  options.static
    ?.map((path) => (isAbsolute(path) ? path : join(process.cwd(), path)))
    .forEach((dir) => app.use(sirv(dir, { dev: !isProduction })))

  // CORS
  app
    .use((req, res, next) => {
      return cors({
        allowedHeaders: req.headers['access-control-request-headers']
          ?.split(',')
          .map((h) => h.trim()),
      })(req, res, next)
    })
    .options('*', cors())

  // Body parser
  app.use(json())

  app.get('/', (_req, res) => res.send(eta.render('index.html', { data: db.data })))

  app.get('/:name', (req, res, next) => {
    const { name = '' } = req.params
    const { where, sort, page, perPage, embed } = parseListParams(req)

    res.locals['data'] = service.find(name, {
      where,
      sort,
      page,
      perPage,
      embed,
    })
    next?.()
  })

  app.get('/:name/:id', (req, res, next) => {
    const { name = '', id = '' } = req.params
    res.locals['data'] = service.findById(name, id, req.query)
    next?.()
  })

  app.post('/:name', withBody(service.create.bind(service)))

  app.put('/:name', withBody(service.update.bind(service)))

  app.put('/:name/:id', withIdAndBody(service.updateById.bind(service)))

  app.patch('/:name', withBody(service.patch.bind(service)))

  app.patch('/:name/:id', withIdAndBody(service.patchById.bind(service)))

  app.delete('/:name/:id', async (req, res, next) => {
    const { name = '', id = '' } = req.params
    res.locals['data'] = await service.destroyById(name, id, req.query['_dependent'])
    next?.()
  })

  app.use('/:name', (req, res) => {
    const { data } = res.locals
    if (data === undefined) {
      res.sendStatus(404)
    } else {
      if (req.method === 'POST') res.status(201)
      res.json(data)
    }
  })

  return app
}


================================================
FILE: src/bin.ts
================================================
#!/usr/bin/env node
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { extname } from "node:path";
import { parseArgs } from "node:util";

import chalk from "chalk";
import { watch } from "chokidar";
import JSON5 from "json5";
import { Low } from "lowdb";
import type { Adapter } from "lowdb";
import { DataFile, JSONFile } from "lowdb/node";
import type { PackageJson } from "type-fest";

import { fileURLToPath } from "node:url";
import { NormalizedAdapter } from "./adapters/normalized-adapter.ts";
import type { RawData } from "./adapters/normalized-adapter.ts";
import { Observer } from "./adapters/observer.ts";
import { createApp } from "./app.ts";
import type { Data } from "./service.ts";

function help() {
  console.log(`Usage: json-server [options] <file>

Options:
  -p, --port <port>  Port (default: 3000)
  -h, --host <host>  Host (default: localhost)
  -s, --static <dir> Static files directory (multiple allowed)
  --help             Show this message
  --version          Show version number
`);
}

// Parse args
function args(): {
  file: string;
  port: number;
  host: string;
  static: string[];
} {
  try {
    const { values, positionals } = parseArgs({
      options: {
        port: {
          type: "string",
          short: "p",
          default: process.env["PORT"] ?? "3000",
        },
        host: {
          type: "string",
          short: "h",
          default: process.env["HOST"] ?? "localhost",
        },
        static: {
          type: "string",
          short: "s",
          multiple: true,
          default: [],
        },
        help: {
          type: "boolean",
        },
        version: {
          type: "boolean",
        },
        // Deprecated
        watch: {
          type: "boolean",
          short: "w",
        },
      },
      allowPositionals: true,
    });

    // --version
    if (values.version) {
      const pkg = JSON.parse(
        readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf-8"),
      ) as PackageJson;
      console.log(pkg.version);
      process.exit();
    }

    // Handle --watch
    if (values.watch) {
      console.log(
        chalk.yellow(
          "--watch/-w can be omitted, JSON Server 1+ watches for file changes by default",
        ),
      );
    }

    if (values.help || positionals.length === 0) {
      help();
      process.exit();
    }

    // App args and options
    return {
      file: positionals[0] ?? "",
      port: parseInt(values.port as string),
      host: values.host as string,
      static: values.static as string[],
    };
  } catch (e) {
    if ((e as NodeJS.ErrnoException).code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
      console.log(chalk.red((e as NodeJS.ErrnoException).message.split(".")[0]));
      help();
      process.exit(1);
    } else {
      throw e;
    }
  }
}

const { file, port, host, static: staticArr } = args();

if (!existsSync(file)) {
  console.log(chalk.red(`File ${file} not found`));
  process.exit(1);
}

// Handle empty string JSON file
if (readFileSync(file, "utf-8").trim() === "") {
  writeFileSync(file, "{}");
}

// Set up database
let adapter: Adapter<RawData>;
if (extname(file) === ".json5") {
  adapter = new DataFile<RawData>(file, {
    parse: JSON5.parse,
    stringify: JSON5.stringify,
  });
} else {
  adapter = new JSONFile<RawData>(file);
}
const observer = new Observer(new NormalizedAdapter(adapter));

const db = new Low<Data>(observer, {});
await db.read();

// Create app
const app = createApp(db, { logger: false, static: staticArr });

function logRoutes(data: Data) {
  console.log(chalk.bold("Endpoints:"));
  if (Object.keys(data).length === 0) {
    console.log(chalk.gray(`No endpoints found, try adding some data to ${file}`));
    return;
  }
  console.log(
    Object.keys(data)
      .map((key) => `${chalk.gray(`http://${host}:${port}/`)}${chalk.blue(key)}`)
      .join("\n"),
  );
}

const kaomojis = ["♡⸜(˶˃ ᵕ ˂˶)⸝♡", "♡( ◡‿◡ )", "( ˶ˆ ᗜ ˆ˵ )", "(˶ᵔ ᵕ ᵔ˶)"];

function randomItem(items: string[]): string {
  const index = Math.floor(Math.random() * items.length);
  return items.at(index) ?? "";
}

app.listen(port, () => {
  console.log(
    [
      chalk.bold(`JSON Server started on PORT :${port}`),
      chalk.gray("Press CTRL-C to stop"),
      chalk.gray(`Watching ${file}...`),
      "",
      chalk.magenta(randomItem(kaomojis)),
      "",
      chalk.bold("Index:"),
      chalk.gray(`http://localhost:${port}/`),
      "",
      chalk.bold("Static files:"),
      chalk.gray("Serving ./public directory if it exists"),
      "",
    ].join("\n"),
  );
  logRoutes(db.data);
});

// Watch file for changes
if (process.env["NODE_ENV"] !== "production") {
  let writing = false; // true if the file is being written to by the app
  let hadReadError = false;
  let prevEndpoints = "";

  observer.onWriteStart = () => {
    writing = true;
  };
  observer.onWriteEnd = () => {
    writing = false;
  };
  observer.onReadStart = () => {
    prevEndpoints = JSON.stringify(Object.keys(db.data).sort());
  };
  observer.onReadEnd = (data) => {
    if (data === null) {
      return;
    }

    const nextEndpoints = JSON.stringify(Object.keys(data).sort());
    if (hadReadError || prevEndpoints !== nextEndpoints) {
      console.log();
      logRoutes(data);
    }
    hadReadError = false;
  };
  watch(file).on("change", () => {
    // Do no reload if the file is being written to by the app
    if (!writing) {
      db.read().catch((e) => {
        if (e instanceof SyntaxError) {
          hadReadError = true;
          return console.log(chalk.red(["", `Error parsing ${file}`, e.message].join("\n")));
        }
        console.log(e);
      });
    }
  });
}


================================================
FILE: src/matches-where.test.ts
================================================
import assert from 'node:assert/strict'
import test from 'node:test'

import type { JsonObject } from 'type-fest'

import { matchesWhere } from './matches-where.ts'

await test('matchesWhere', async (t) => {
  const obj: JsonObject = { a: 10, b: 20, c: 'x', nested: { a: 10, b: 20 } }
  const cases: [JsonObject, boolean][] = [
    [{ a: { eq: 10 } }, true],
    [{ a: { eq: 11 } }, false],
    [{ c: { ne: 'y' } }, true],
    [{ c: { ne: 'x' } }, false],
    [{ a: { lt: 11 } }, true],
    [{ a: { lt: 10 } }, false],
    [{ a: { lte: 10 } }, true],
    [{ a: { lte: 9 } }, false],
    [{ b: { gt: 19 } }, true],
    [{ b: { gt: 20 } }, false],
    [{ b: { gte: 20 } }, true],
    [{ b: { gte: 21 } }, false],
    [{ a: { gt: 0 }, b: { lt: 30 } }, true],
    [{ a: { gt: 10 }, b: { lt: 30 } }, false],
    [{ or: [{ a: { lt: 0 } }, { b: { gt: 19 } }] }, true],
    [{ or: [{ a: { lt: 0 } }, { b: { gt: 20 } }] }, false],
    [{ nested: { a: { eq: 10 } } }, true],
    [{ nested: { b: { lt: 20 } } }, false],
    [{ a: { in: [10, 11] } }, true],
    [{ a: { in: [1, 2] } }, false],
    [{ c: { in: ['x', 'y'] } }, true],
    [{ c: { in: ['y', 'z'] } }, false],
    [{ a: { in: [10, 11], gt: 9 } }, true],
    [{ a: { in: [10, 11], gt: 10 } }, false],
    [{ a: { foo: 10 } }, true],
    [{ a: { foo: 10, eq: 10 } }, true],
    [{ missing: { foo: 1 } }, true],
    // contains
    [{ c: { contains: 'x' } }, true],
    [{ c: { contains: 'X' } }, true],
    [{ c: { contains: 'z' } }, false],
    [{ a: { contains: '1' } }, false],
    [{ c: { contains: 1 } }, false],
    // startsWith
    [{ c: { startsWith: 'x' } }, true],
    [{ c: { startsWith: 'X' } }, true],
    [{ c: { startsWith: 'z' } }, false],
    [{ a: { startsWith: '1' } }, false],
    [{ c: { startsWith: 1 } }, false],
    // endsWith
    [{ c: { endsWith: 'x' } }, true],
    [{ c: { endsWith: 'X' } }, true],
    [{ c: { endsWith: 'z' } }, false],
    [{ a: { endsWith: '1' } }, false],
    [{ c: { endsWith: 1 } }, false],
  ]

  for (const [query, expected] of cases) {
    await t.test(JSON.stringify(query), () => {
      assert.equal(matchesWhere(obj, query), expected)
    })
  }
})


================================================
FILE: src/matches-where.ts
================================================
import type { JsonObject } from 'type-fest'

import { WHERE_OPERATORS, type WhereOperator } from './where-operators.ts'

type OperatorObject = Partial<Record<WhereOperator, unknown>>

function isJSONObject(value: unknown): value is JsonObject {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function getKnownOperators(value: unknown): WhereOperator[] {
  if (!isJSONObject(value)) return []

  const ops: WhereOperator[] = []
  for (const op of WHERE_OPERATORS) {
    if (op in value) {
      ops.push(op)
    }
  }

  return ops
}

export function matchesWhere(obj: JsonObject, where: JsonObject): boolean {
  for (const [key, value] of Object.entries(where)) {
    if (key === 'or') {
      if (!Array.isArray(value) || value.length === 0) return false

      let matched = false
      for (const subWhere of value) {
        if (isJSONObject(subWhere) && matchesWhere(obj, subWhere)) {
          matched = true
          break
        }
      }

      if (!matched) return false
      continue
    }

    const field = (obj as Record<string, unknown>)[key]

    if (isJSONObject(value)) {
      const knownOps = getKnownOperators(value)

      if (knownOps.length > 0) {
        if (field === undefined) return false

        const op = value as OperatorObject
        if (knownOps.includes('lt') && !((field as any) < (op.lt as any))) return false
        if (knownOps.includes('lte') && !((field as any) <= (op.lte as any))) return false
        if (knownOps.includes('gt') && !((field as any) > (op.gt as any))) return false
        if (knownOps.includes('gte') && !((field as any) >= (op.gte as any))) return false
        if (knownOps.includes('eq') && !((field as any) === (op.eq as any))) return false
        if (knownOps.includes('ne') && !((field as any) !== (op.ne as any))) return false
        if (knownOps.includes('in')) {
          const inValues = Array.isArray(op.in) ? op.in : [op.in]
          if (!inValues.some((v) => (field as any) === (v as any))) return false
        }
        if (knownOps.includes('contains')) {
          if (typeof field !== 'string') return false
          if (!field.toLowerCase().includes(String(op.contains).toLowerCase())) return false
        }
        if (knownOps.includes('startsWith')) {
          if (typeof field !== 'string') return false
          if (!field.toLowerCase().startsWith(String(op.startsWith).toLowerCase())) return false
        }
        if (knownOps.includes('endsWith')) {
          if (typeof field !== 'string') return false
          if (!field.toLowerCase().endsWith(String(op.endsWith).toLowerCase())) return false
        }
        continue
      }

      if (isJSONObject(field)) {
        if (!matchesWhere(field, value)) return false
      }

      continue
    }

    if (field === undefined) return false

    return false
  }

  return true
}


================================================
FILE: src/paginate.test.ts
================================================
import assert from 'node:assert/strict'
import test from 'node:test'

import { paginate } from './paginate.ts'

await test('paginate', async (t) => {
  // Pagination: page boundaries and clamping behavior.
  const cases = [
    {
      name: 'page=1 perPage=2 items=5 -> [1,2]',
      items: [1, 2, 3, 4, 5],
      page: 1,
      perPage: 2,
      expected: {
        first: 1,
        prev: null,
        next: 2,
        last: 3,
        pages: 3,
        items: 5,
        data: [1, 2],
      },
    },
    {
      name: 'page=2 perPage=2 items=5 -> [3,4]',
      items: [1, 2, 3, 4, 5],
      page: 2,
      perPage: 2,
      expected: {
        first: 1,
        prev: 1,
        next: 3,
        last: 3,
        pages: 3,
        items: 5,
        data: [3, 4],
      },
    },
    {
      name: 'page=9 perPage=2 items=5 -> clamp to last',
      items: [1, 2, 3, 4, 5],
      page: 9,
      perPage: 2,
      expected: {
        first: 1,
        prev: 2,
        next: null,
        last: 3,
        pages: 3,
        items: 5,
        data: [5],
      },
    },
    {
      name: 'page=0 perPage=2 items=3 -> clamp to first',
      items: [1, 2, 3],
      page: 0,
      perPage: 2,
      expected: {
        first: 1,
        prev: null,
        next: 2,
        last: 2,
        pages: 2,
        items: 3,
        data: [1, 2],
      },
    },
    {
      name: 'items=[] page=1 perPage=2 -> stable empty pagination',
      items: [],
      page: 1,
      perPage: 2,
      expected: {
        first: 1,
        prev: null,
        next: null,
        last: 1,
        pages: 1,
        items: 0,
        data: [],
      },
    },
    {
      name: 'perPage=0 -> clamp perPage to 1',
      items: [1, 2, 3],
      page: 1,
      perPage: 0,
      expected: {
        first: 1,
        prev: null,
        next: 2,
        last: 3,
        pages: 3,
        items: 3,
        data: [1],
      },
    },
  ]

  for (const tc of cases) {
    await t.test(tc.name, () => {
      const res = paginate(tc.items, tc.page, tc.perPage)
      assert.deepEqual(res, tc.expected)
    })
  }
})


================================================
FILE: src/paginate.ts
================================================
export type PaginationResult<T> = {
  first: number
  prev: number | null
  next: number | null
  last: number
  pages: number
  items: number
  data: T[]
}

export function paginate<T>(items: T[], page: number, perPage: number): PaginationResult<T> {
  const totalItems = items.length
  const safePerPage = Number.isFinite(perPage) && perPage > 0 ? Math.floor(perPage) : 1
  const pages = Math.max(1, Math.ceil(totalItems / safePerPage))

  // Ensure page is within the valid range
  const safePage = Number.isFinite(page) ? Math.floor(page) : 1
  const currentPage = Math.max(1, Math.min(safePage, pages))

  const first = 1
  const prev = currentPage > 1 ? currentPage - 1 : null
  const next = currentPage < pages ? currentPage + 1 : null
  const last = pages

  const start = (currentPage - 1) * safePerPage
  const end = start + safePerPage
  const data = items.slice(start, end)

  return {
    first,
    prev,
    next,
    last,
    pages,
    items: totalItems,
    data,
  }
}


================================================
FILE: src/parse-where.test.ts
================================================
import assert from 'node:assert/strict'
import test from 'node:test'

import { parseWhere } from './parse-where.ts'

await test('parseWhere', async (t) => {
  const cases: [string, Record<string, unknown>][] = [
    [
      'views:gt=100&title:eq=a',
      {
        views: { gt: 100 },
        title: { eq: 'a' },
      },
    ],
    [
      'title=hello',
      {
        title: { eq: 'hello' },
      },
    ],
    [
      'author.name:lt=c&author.id:ne=2',
      {
        author: {
          name: { lt: 'c' },
          id: { ne: 2 },
        },
      },
    ],
    [
      'views:gt=100&views:lt=300',
      {
        views: { gt: 100, lt: 300 },
      },
    ],
    [
      'name:eq=Alice',
      {
        name: { eq: 'Alice' },
      },
    ],
    [
      'views:gt=100&published:eq=true&ratio:lt=0.5&deleted:eq=null',
      {
        views: { gt: 100 },
        published: { eq: true },
        ratio: { lt: 0.5 },
        deleted: { eq: null },
      },
    ],
    [
      'views:foo=100&title:eq=a',
      {
        title: { eq: 'a' },
      },
    ],
    ['views:foo=100', {}],
    [
      'views_gt=100&title_eq=a',
      {
        views: { gt: 100 },
        title: { eq: 'a' },
      },
    ],
    [
      'first_name_eq=Alice&author.first_name_ne=Bob',
      {
        first_name: { eq: 'Alice' },
        author: {
          first_name: { ne: 'Bob' },
        },
      },
    ],
    [
      'first_name=Alice',
      {
        first_name: { eq: 'Alice' },
      },
    ],
    [
      'views_gt=100&views:lt=300',
      {
        views: { gt: 100, lt: 300 },
      },
    ],
    [
      'id:in=1,3',
      {
        id: { in: [1, 3] },
      },
    ],
    [
      'title_in=hello,world',
      {
        title: { in: ['hello', 'world'] },
      },
    ],
    [
      'title:contains=ello',
      {
        title: { contains: 'ello' },
      },
    ],
    [
      'title:startsWith=hel',
      {
        title: { startsWith: 'hel' },
      },
    ],
    [
      'title:endsWith=rld',
      {
        title: { endsWith: 'rld' },
      },
    ],
  ]

  for (const [query, expected] of cases) {
    await t.test(query, () => {
      assert.deepEqual(parseWhere(query), expected)
    })
  }
})


================================================
FILE: src/parse-where.ts
================================================
import { setProperty } from 'dot-prop'
import type { JsonObject } from 'type-fest'

import { isWhereOperator, type WhereOperator } from './where-operators.ts'

function splitKey(key: string): { path: string; op: WhereOperator | null } {
  const colonIdx = key.lastIndexOf(':')
  if (colonIdx !== -1) {
    const path = key.slice(0, colonIdx)
    const op = key.slice(colonIdx + 1)
    if (!op) {
      return { path: key, op: 'eq' }
    }

    return isWhereOperator(op) ? { path, op } : { path, op: null }
  }

  // Compatibility with v0.17 operator style (e.g. _lt, _gt)
  const underscoreMatch = key.match(/^(.*)_([a-z]+)$/)
  if (underscoreMatch) {
    const path = underscoreMatch[1]
    const op = underscoreMatch[2]
    if (path && isWhereOperator(op)) {
      return { path, op }
    }
  }

  return { path: key, op: 'eq' }
}

function setPathOp(root: JsonObject, path: string, op: WhereOperator, value: string): void {
  const fullPath = `${path}.${op}`
  if (op === 'in') {
    setProperty(
      root,
      fullPath,
      value.split(',').map((part) => coerceValue(part.trim())),
    )
    return
  }

  setProperty(root, fullPath, coerceValue(value))
}

function coerceValue(value: string): string | number | boolean | null {
  if (value === 'true') return true
  if (value === 'false') return false
  if (value === 'null') return null

  if (value.trim() === '') return value

  const num = Number(value)
  if (Number.isFinite(num)) return num

  return value
}

export function parseWhere(query: string): JsonObject {
  const out: JsonObject = {}
  const params = new URLSearchParams(query)

  for (const [rawKey, rawValue] of params.entries()) {
    const { path, op } = splitKey(rawKey)
    if (op === null) continue
    setPathOp(out, path, op, rawValue)
  }

  return out
}


================================================
FILE: src/random-id.ts
================================================
import { randomBytes } from 'node:crypto'

export function randomId(): string {
  return randomBytes(2).toString('hex')
}


================================================
FILE: src/service.test.ts
================================================
import assert from 'node:assert/strict'
import test, { beforeEach } from 'node:test'

import { Low, Memory } from 'lowdb'
import type { JsonObject } from 'type-fest'

import type { Data } from './service.ts'
import { Service } from './service.ts'

const defaultData = { posts: [], comments: [], object: {} }
const adapter = new Memory<Data>()
const db = new Low<Data>(adapter, defaultData)
const service = new Service(db)

const POSTS = 'posts'
const COMMENTS = 'comments'
const OBJECT = 'object'

const UNKNOWN_RESOURCE = 'xxx'
const UNKNOWN_ID = 'xxx'

const post1 = {
  id: '1',
  title: 'a',
  views: 100,
  published: true,
  author: { name: 'foo' },
  tags: ['foo', 'bar'],
}
const post2 = {
  id: '2',
  title: 'b',
  views: 200,
  published: false,
  author: { name: 'bar' },
  tags: ['bar'],
}
const post3 = {
  id: '3',
  title: 'c',
  views: 300,
  published: false,
  author: { name: 'baz' },
  tags: ['foo'],
}
const comment1 = { id: '1', title: 'a', postId: '1' }
const obj = {
  f1: 'foo',
}

beforeEach(() => {
  db.data = structuredClone({
    posts: [post1, post2, post3],
    comments: [comment1],
    object: obj,
  })
})

await test('findById', () => {
  const cases: [[string, string, { _embed?: string[] | string }], unknown][] = [
    [[POSTS, '1', {}], db.data?.[POSTS]?.[0]],
    [[POSTS, UNKNOWN_ID, {}], undefined],
    [[POSTS, '1', { _embed: ['comments'] }], { ...post1, comments: [comment1] }],
    [[COMMENTS, '1', { _embed: ['post'] }], { ...comment1, post: post1 }],
    [[UNKNOWN_RESOURCE, '1', {}], undefined],
  ]

  for (const [[name, id, query], expected] of cases) {
    assert.deepEqual(service.findById(name, id, query), expected)
  }
})

await test('find', async (t) => {
  const whereFromPayload = JSON.parse('{"author":{"name":{"eq":"bar"}}}') as JsonObject

  const cases: [{ where: JsonObject; sort?: string; page?: number; perPage?: number }, unknown][] =
    [
      [{ where: { title: { eq: 'b' } } }, [post2]],
      [{ where: whereFromPayload }, [post2]],
      [{ where: {}, sort: '-views' }, [post3, post2, post1]],
      [
        { where: {}, page: 2, perPage: 2 },
        {
          first: 1,
          prev: 1,
          next: null,
          last: 2,
          pages: 2,
          items: 3,
          data: [post3],
        },
      ],
    ]

  for (const [opts, expected] of cases) {
    await t.test(JSON.stringify(opts), () => {
      assert.deepEqual(service.find(POSTS, opts), expected)
    })
  }
})

await test('create', async () => {
  const post = { title: 'new post' }
  const res = await service.create(POSTS, post)
  assert.equal(res?.['title'], post.title)
  assert.equal(typeof res?.['id'], 'string', 'id should be a string')

  assert.equal(await service.create(UNKNOWN_RESOURCE, post), undefined)
})

await test('update', async () => {
  const obj = { f1: 'bar' }
  const res = await service.update(OBJECT, obj)
  assert.equal(res, obj)

  assert.equal(
    await service.update(UNKNOWN_RESOURCE, obj),
    undefined,
    'should ignore unknown resources',
  )
  assert.equal(await service.update(POSTS, {}), undefined, 'should ignore arrays')
})

await test('patch', async () => {
  const obj = { f2: 'bar' }
  const res = await service.patch(OBJECT, obj)
  assert.deepEqual(res, { f1: 'foo', ...obj })

  assert.equal(
    await service.patch(UNKNOWN_RESOURCE, obj),
    undefined,
    'should ignore unknown resources',
  )
  assert.equal(await service.patch(POSTS, {}), undefined, 'should ignore arrays')
})

await test('updateById', async () => {
  const post = { id: 'xxx', title: 'updated post' }
  const res = await service.updateById(POSTS, post1.id, post)
  assert.equal(res?.['id'], post1.id, 'id should not change')
  assert.equal(res?.['title'], post.title)

  assert.equal(await service.updateById(UNKNOWN_RESOURCE, post1.id, post), undefined)
  assert.equal(await service.updateById(POSTS, UNKNOWN_ID, post), undefined)
})

await test('patchById', async () => {
  const post = { id: 'xxx', title: 'updated post' }
  const res = await service.patchById(POSTS, post1.id, post)
  assert.notEqual(res, undefined)
  assert.equal(res?.['id'], post1.id)
  assert.equal(res?.['title'], post.title)

  assert.equal(await service.patchById(UNKNOWN_RESOURCE, post1.id, post), undefined)
  assert.equal(await service.patchById(POSTS, UNKNOWN_ID, post), undefined)
})

await test('destroy', async (t) => {
  await t.test('nullifies foreign keys', async () => {
    const prevLength = Number(db.data?.[POSTS]?.length) || 0
    await service.destroyById(POSTS, post1.id)
    assert.equal(db.data?.[POSTS]?.length, prevLength - 1)
    assert.deepEqual(db.data?.[COMMENTS], [{ ...comment1, postId: null }])
  })

  await t.test('deletes dependent resources', async () => {
    const prevLength = Number(db.data?.[POSTS]?.length) || 0
    await service.destroyById(POSTS, post1.id, [COMMENTS])
    assert.equal(db.data[POSTS].length, prevLength - 1)
    assert.equal(db.data[COMMENTS].length, 0)
  })

  await t.test('ignores unknown resources', async () => {
    assert.equal(await service.destroyById(UNKNOWN_RESOURCE, post1.id), undefined)
    assert.equal(await service.destroyById(POSTS, UNKNOWN_ID), undefined)
  })
})


================================================
FILE: src/service.ts
================================================
import inflection from 'inflection'
import { Low } from 'lowdb'
import sortOn from 'sort-on'
import type { JsonObject } from 'type-fest'

import { matchesWhere } from './matches-where.ts'
import { paginate, type PaginationResult } from './paginate.ts'
import { randomId } from './random-id.ts'
export type Item = Record<string, unknown>

export type Data = Record<string, Item[] | Item>

export function isItem(obj: unknown): obj is Item {
  return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
}

export type PaginatedItems = PaginationResult<Item>

function ensureArray(arg: string | string[] = []): string[] {
  return Array.isArray(arg) ? arg : [arg]
}

function embed(db: Low<Data>, name: string, item: Item, related: string): Item {
  if (inflection.singularize(related) === related) {
    const relatedData = db.data[inflection.pluralize(related)] as Item[]
    if (!relatedData) {
      return item
    }
    const foreignKey = `${related}Id`
    const relatedItem = relatedData.find((relatedItem: Item) => {
      return relatedItem['id'] === item[foreignKey]
    })
    return { ...item, [related]: relatedItem }
  }
  const relatedData: Item[] = db.data[related] as Item[]

  if (!relatedData) {
    return item
  }

  const foreignKey = `${inflection.singularize(name)}Id`
  const relatedItems = relatedData.filter(
    (relatedItem: Item) => relatedItem[foreignKey] === item['id'],
  )

  return { ...item, [related]: relatedItems }
}

function nullifyForeignKey(db: Low<Data>, name: string, id: string) {
  const foreignKey = `${inflection.singularize(name)}Id`

  Object.entries(db.data).forEach(([key, items]) => {
    // Skip
    if (key === name) return

    // Nullify
    if (Array.isArray(items)) {
      items.forEach((item) => {
        if (item[foreignKey] === id) {
          item[foreignKey] = null
        }
      })
    }
  })
}

function deleteDependents(db: Low<Data>, name: string, dependents: string[]) {
  const foreignKey = `${inflection.singularize(name)}Id`

  Object.entries(db.data).forEach(([key, items]) => {
    // Skip
    if (key === name || !dependents.includes(key)) return

    // Delete if foreign key is null
    if (Array.isArray(items)) {
      db.data[key] = items.filter((item) => item[foreignKey] !== null)
    }
  })
}

export class Service {
  #db: Low<Data>

  constructor(db: Low<Data>) {
    this.#db = db
  }

  #get(name: string): Item[] | Item | undefined {
    return this.#db.data[name]
  }

  has(name: string): boolean {
    return Object.prototype.hasOwnProperty.call(this.#db?.data, name)
  }

  findById(name: string, id: string, query: { _embed?: string[] | string }): Item | undefined {
    const value = this.#get(name)

    if (Array.isArray(value)) {
      let item = value.find((item) => item['id'] === id)
      ensureArray(query._embed).forEach((related) => {
        if (item !== undefined) item = embed(this.#db, name, item, related)
      })
      return item
    }

    return
  }

  find(
    name: string,
    opts: {
      where: JsonObject
      sort?: string
      page?: number
      perPage?: number
      embed?: string | string[]
    },
  ): Item[] | PaginatedItems | Item | undefined {
    const items = this.#get(name)

    if (!Array.isArray(items)) {
      return items
    }

    let results = items

    // Include
    ensureArray(opts.embed).forEach((related) => {
      results = results.map((item) => embed(this.#db, name, item, related))
    })

    results = results.filter((item) => matchesWhere(item as JsonObject, opts.where))
    if (opts.sort) {
      results = sortOn(results, opts.sort.split(','))
    }

    if (opts.page !== undefined) {
      return paginate(results, opts.page, opts.perPage ?? 10)
    }

    return results
  }

  async create(name: string, data: Omit<Item, 'id'> = {}): Promise<Item | undefined> {
    const items = this.#get(name)
    if (items === undefined || !Array.isArray(items)) return

    const item = { id: randomId(), ...data }
    items.push(item)

    await this.#db.write()
    return item
  }

  async #updateOrPatch(name: string, body: Item = {}, isPatch: boolean): Promise<Item | undefined> {
    const item = this.#get(name)
    if (item === undefined || Array.isArray(item)) return

    const nextItem = (this.#db.data[name] = isPatch ? { ...item, ...body } : body)

    await this.#db.write()
    return nextItem
  }

  async #updateOrPatchById(
    name: string,
    id: string,
    body: Item = {},
    isPatch: boolean,
  ): Promise<Item | undefined> {
    const items = this.#get(name)
    if (items === undefined || !Array.isArray(items)) return

    const item = items.find((item) => item['id'] === id)
    if (!item) return

    const nextItem = isPatch ? { ...item, ...body, id } : { ...body, id }
    const index = items.indexOf(item)
    items.splice(index, 1, nextItem)

    await this.#db.write()
    return nextItem
  }

  async update(name: string, body: Item = {}): Promise<Item | undefined> {
    return this.#updateOrPatch(name, body, false)
  }

  async patch(name: string, body: Item = {}): Promise<Item | undefined> {
    return this.#updateOrPatch(name, body, true)
  }

  async updateById(name: string, id: string, body: Item = {}): Promise<Item | undefined> {
    return this.#updateOrPatchById(name, id, body, false)
  }

  async patchById(name: string, id: string, body: Item = {}): Promise<Item | undefined> {
    return this.#updateOrPatchById(name, id, body, true)
  }

  async destroyById(
    name: string,
    id: string,
    dependent?: string | string[],
  ): Promise<Item | undefined> {
    const items = this.#get(name)
    if (items === undefined || !Array.isArray(items)) return

    const item = items.find((item) => item['id'] === id)
    if (item === undefined) return
    const index = items.indexOf(item)
    items.splice(index, 1)

    nullifyForeignKey(this.#db, name, id)
    const dependents = ensureArray(dependent)
    deleteDependents(this.#db, name, dependents)

    await this.#db.write()
    return item
  }
}


================================================
FILE: src/where-operators.ts
================================================
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)
}


================================================
FILE: tsconfig.json
================================================
{
  "exclude": ["src/**/*.test.ts"],
  "compilerOptions": {
    "outDir": "./lib",
    "target": "esnext",
    "module": "nodenext",
    "skipLibCheck": true,
    "strict": true,
    "allowImportingTsExtensions": true,
    "rewriteRelativeImportExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true
  }
}


================================================
FILE: views/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JSON Server</title>
    <style>
      :root {
        color-scheme: light dark;
        --fg: light-dark(#111, #e8e8e8);
        --bg: light-dark(#fff, #111214);
        --muted: light-dark(#999, #9aa0a6);
        --line: light-dark(#e5e5e5, #2c2f34);
        --accent: light-dark(#6366f1, #8b90ff);
        --chip-bg: light-dark(#111, #f5f5f5);
        --chip-fg: light-dark(#fff, #111214);
      }
      body {
        margin: 40px auto;
        max-width: 400px;
        padding: 0 24px;
        font-family:
          ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
        background-color: var(--bg);
        color: var(--fg);
        line-height: 1.5;
        font-size: 14px;
        transition:
          background-color 0.2s ease,
          color 0.2s ease,
          border-color 0.2s ease;
      }
      a {
        color: inherit;
      }
      header {
        margin-bottom: 40px;
        color: var(--muted);
      }
      .topbar {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        margin-bottom: 12px;
        padding-top: 12px;
        border-top: 1px solid var(--line);
      }
      .topbar a {
        text-decoration: none;
        transition: color 0.1s ease;
      }
      .topbar a:first-child {
        color: var(--fg);
      }
      .topbar a:hover {
        color: var(--accent);
      }
      .section-title {
        margin-bottom: 8px;
        color: var(--muted);
      }
      .list {
        display: flex;
        flex-direction: column;
        gap: 4px;
      }
      .list a {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 8px 0;
        text-decoration: none;
        transition: color 0.1s ease;
      }
      .list a:hover {
        color: var(--accent);
      }
      .meta {
        color: var(--muted);
        font-size: 13px;
      }
      .empty {
        color: var(--muted);
      }
      footer {
        margin-top: 48px;
        padding-top: 12px;
        border-top: 1px solid var(--line);
        color: var(--muted);
        font-size: 13px;
      }
      .heart {
        position: fixed;
        bottom: 20px;
        right: 24px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 32px;
        height: 32px;
        border-radius: 50%;
        color: var(--chip-fg);
        background-color: var(--chip-bg);
        text-decoration: none;
        transition: transform 0.15s ease;
      }
      .heart:hover {
        transform: scale(1.1);
      }
    </style>
  </head>

  <body>
    <% const resources = Object.entries(it.data ?? {}); %>

    <header>
      <div class="topbar">
        <a href="https://github.com/typicode/json-server" target="_blank">json-server</a>
        <a href="https://github.com/typicode/json-server" target="_blank">README</a>
      </div>
      <div class="intro">Available REST resources from db.json.</div>
    </header>

    <div class="section-title">Resources</div>

    <div class="list">
      <% if (resources.length === 0) { %>
      <span class="empty">No resources in db.json.</span>
      <% } else { %> <% resources.forEach(function([name, value]) { const isCollection =
      Array.isArray(value); %>
      <a href="/<%= name %>">
        <span>/<%= name %></span>
        <span class="meta">
          <% if (isCollection) { %> <%= value.length %> items <% } else { %> object <% } %>
        </span>
      </a>
      <% }) %> <% } %>
    </div>

    <footer>
      <span>To replace this page, create public/index.html.</span>
    </footer>

    <a href="https://github.com/sponsors/typicode" target="_blank" class="heart">❤</a>

  </body>
</html>
Download .txt
gitextract_grojiw9z/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── node.js.yml
│       └── publish.yml
├── .gitignore
├── .husky/
│   └── pre-commit
├── .oxfmtrc.json
├── LICENSE
├── README.md
├── fixtures/
│   ├── db.json
│   └── db.json5
├── package.json
├── public/
│   └── test.html
├── schema.json
├── src/
│   ├── adapters/
│   │   ├── normalized-adapter.test.ts
│   │   ├── normalized-adapter.ts
│   │   └── observer.ts
│   ├── app.test.ts
│   ├── app.ts
│   ├── bin.ts
│   ├── matches-where.test.ts
│   ├── matches-where.ts
│   ├── paginate.test.ts
│   ├── paginate.ts
│   ├── parse-where.test.ts
│   ├── parse-where.ts
│   ├── random-id.ts
│   ├── service.test.ts
│   ├── service.ts
│   └── where-operators.ts
├── tsconfig.json
└── views/
    └── index.html
Download .txt
SYMBOL INDEX (68 symbols across 13 files)

FILE: src/adapters/normalized-adapter.test.ts
  class StubAdapter (line 10) | class StubAdapter implements Adapter<RawData> {
    method constructor (line 13) | constructor(data: RawData | null) {
    method read (line 17) | async read(): Promise<RawData | null> {
    method write (line 21) | async write(data: RawData): Promise<void> {
    method data (line 25) | get data(): RawData | null {

FILE: src/adapters/normalized-adapter.ts
  constant DEFAULT_SCHEMA_PATH (line 6) | const DEFAULT_SCHEMA_PATH = './node_modules/json-server/schema.json'
  type RawData (line 7) | type RawData = Record<string, Item[] | Item | string | undefined> & {
  class NormalizedAdapter (line 11) | class NormalizedAdapter implements Adapter<Data> {
    method constructor (line 14) | constructor(adapter: Adapter<RawData>) {
    method read (line 18) | async read(): Promise<Data | null> {
    method write (line 44) | async write(data: Data): Promise<void> {

FILE: src/adapters/observer.ts
  class Observer (line 4) | class Observer<T> {
    method constructor (line 20) | constructor(adapter: Adapter<T>) {
    method read (line 24) | async read() {
    method write (line 31) | async write(arg: T) {

FILE: src/app.test.ts
  type Test (line 13) | type Test = {
  type HTTPMethods (line 19) | type HTTPMethods = 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' ...

FILE: src/app.ts
  type AppOptions (line 18) | type AppOptions = {
  constant RESERVED_QUERY_KEYS (line 28) | const RESERVED_QUERY_KEYS = new Set(['_sort', '_page', '_per_page', '_em...
  function parseListParams (line 30) | function parseListParams(req: any) {
  function withBody (line 68) | function withBody(action: (name: string, body: Record<string, unknown>) ...
  function withIdAndBody (line 80) | function withIdAndBody(
  function createApp (line 94) | function createApp(db: Low<Data>, options: AppOptions = {}) {

FILE: src/bin.ts
  function help (line 21) | function help() {
  function args (line 34) | function args(): {
  function logRoutes (line 145) | function logRoutes(data: Data) {
  function randomItem (line 160) | function randomItem(items: string[]): string {

FILE: src/matches-where.ts
  type OperatorObject (line 5) | type OperatorObject = Partial<Record<WhereOperator, unknown>>
  function isJSONObject (line 7) | function isJSONObject(value: unknown): value is JsonObject {
  function getKnownOperators (line 11) | function getKnownOperators(value: unknown): WhereOperator[] {
  function matchesWhere (line 24) | function matchesWhere(obj: JsonObject, where: JsonObject): boolean {

FILE: src/paginate.ts
  type PaginationResult (line 1) | type PaginationResult<T> = {
  function paginate (line 11) | function paginate<T>(items: T[], page: number, perPage: number): Paginat...

FILE: src/parse-where.ts
  function splitKey (line 6) | function splitKey(key: string): { path: string; op: WhereOperator | null...
  function setPathOp (line 31) | function setPathOp(root: JsonObject, path: string, op: WhereOperator, va...
  function coerceValue (line 45) | function coerceValue(value: string): string | number | boolean | null {
  function parseWhere (line 58) | function parseWhere(query: string): JsonObject {

FILE: src/random-id.ts
  function randomId (line 3) | function randomId(): string {

FILE: src/service.test.ts
  constant POSTS (line 15) | const POSTS = 'posts'
  constant COMMENTS (line 16) | const COMMENTS = 'comments'
  constant OBJECT (line 17) | const OBJECT = 'object'
  constant UNKNOWN_RESOURCE (line 19) | const UNKNOWN_RESOURCE = 'xxx'
  constant UNKNOWN_ID (line 20) | const UNKNOWN_ID = 'xxx'

FILE: src/service.ts
  type Item (line 9) | type Item = Record<string, unknown>
  type Data (line 11) | type Data = Record<string, Item[] | Item>
  function isItem (line 13) | function isItem(obj: unknown): obj is Item {
  type PaginatedItems (line 17) | type PaginatedItems = PaginationResult<Item>
  function ensureArray (line 19) | function ensureArray(arg: string | string[] = []): string[] {
  function embed (line 23) | function embed(db: Low<Data>, name: string, item: Item, related: string)...
  function nullifyForeignKey (line 49) | function nullifyForeignKey(db: Low<Data>, name: string, id: string) {
  function deleteDependents (line 67) | function deleteDependents(db: Low<Data>, name: string, dependents: strin...
  class Service (line 81) | class Service {
    method constructor (line 84) | constructor(db: Low<Data>) {
    method #get (line 88) | #get(name: string): Item[] | Item | undefined {
    method has (line 92) | has(name: string): boolean {
    method findById (line 96) | findById(name: string, id: string, query: { _embed?: string[] | string...
    method find (line 110) | find(
    method create (line 145) | async create(name: string, data: Omit<Item, 'id'> = {}): Promise<Item ...
    method #updateOrPatch (line 156) | async #updateOrPatch(name: string, body: Item = {}, isPatch: boolean):...
    method #updateOrPatchById (line 166) | async #updateOrPatchById(
    method update (line 186) | async update(name: string, body: Item = {}): Promise<Item | undefined> {
    method patch (line 190) | async patch(name: string, body: Item = {}): Promise<Item | undefined> {
    method updateById (line 194) | async updateById(name: string, id: string, body: Item = {}): Promise<I...
    method patchById (line 198) | async patchById(name: string, id: string, body: Item = {}): Promise<It...
    method destroyById (line 202) | async destroyById(

FILE: src/where-operators.ts
  constant WHERE_OPERATORS (line 1) | const WHERE_OPERATORS = [
  type WhereOperator (line 14) | type WhereOperator = (typeof WHERE_OPERATORS)[number]
  function isWhereOperator (line 16) | function isWhereOperator(value: string): value is WhereOperator {
Condensed preview — 32 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (66K chars).
[
  {
    "path": ".gitattributes",
    "chars": 52,
    "preview": "* text=auto eol=lf\n*.ts linguist-language=JavaScript"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 17,
    "preview": "github: typicode\n"
  },
  {
    "path": ".github/workflows/node.js.yml",
    "chars": 399,
    "preview": "name: Node.js CI\non: [push]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n     "
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 600,
    "preview": "name: Publish Package to npmjs\non:\n  release:\n    types: [published]\npermissions:\n  id-token: write  # Required for OIDC"
  },
  {
    "path": ".gitignore",
    "chars": 64,
    "preview": "**/*.log\n.DS_Store\n.idea\nlib\nnode_modules\npublic/output.css\ntmp\n"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 10,
    "preview": "pnpm test\n"
  },
  {
    "path": ".oxfmtrc.json",
    "chars": 44,
    "preview": "{\n  \"semi\": false,\n  \"singleQuote\": true,\n}\n"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2026 typicode\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "README.md",
    "chars": 8189,
    "preview": "# JSON-Server\n\n[![Node.js CI](https://github.com/typicode/json-server/actions/workflows/node.js.yml/badge.svg)](https://"
  },
  {
    "path": "fixtures/db.json",
    "chars": 309,
    "preview": "{\n  \"posts\": [\n    { \"id\": \"1\", \"title\": \"a title\" },\n    { \"id\": \"2\", \"title\": \"another title\" }\n  ],\n  \"comments\": [\n "
  },
  {
    "path": "fixtures/db.json5",
    "chars": 365,
    "preview": "{\n  posts: [\n    {\n      id: \"1\",\n      title: \"a title\",\n    },\n    {\n      id: \"2\",\n      title: \"another title\",\n    "
  },
  {
    "path": "package.json",
    "chars": 1624,
    "preview": "{\n  \"name\": \"json-server\",\n  \"version\": \"1.0.0-beta.13\",\n  \"description\": \"\",\n  \"keywords\": [\n    \"JSON\",\n    \"server\",\n"
  },
  {
    "path": "public/test.html",
    "chars": 73,
    "preview": "<!-- Testing automatic serving of files from the 'public/' directory -->\n"
  },
  {
    "path": "schema.json",
    "chars": 192,
    "preview": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"type\": \"object\",\n  \"additionalProperties\": {\n    \"oneO"
  },
  {
    "path": "src/adapters/normalized-adapter.test.ts",
    "chars": 1737,
    "preview": "import assert from 'node:assert/strict'\nimport test from 'node:test'\n\nimport type { Adapter } from 'lowdb'\n\nimport { DEF"
  },
  {
    "path": "src/adapters/normalized-adapter.ts",
    "chars": 1122,
    "preview": "import type { Adapter } from 'lowdb'\n\nimport { randomId } from '../random-id.ts'\nimport type { Data, Item } from '../ser"
  },
  {
    "path": "src/adapters/observer.ts",
    "chars": 651,
    "preview": "import type { Adapter } from 'lowdb'\n\n// Lowdb adapter to observe read/write events\nexport class Observer<T> {\n  #adapte"
  },
  {
    "path": "src/app.test.ts",
    "chars": 6332,
    "preview": "   import assert from 'node:assert/strict'\nimport { writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimpo"
  },
  {
    "path": "src/app.ts",
    "chars": 4708,
    "preview": "import { dirname, isAbsolute, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport { App } from '@ti"
  },
  {
    "path": "src/bin.ts",
    "chars": 5731,
    "preview": "#!/usr/bin/env node\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { extname } from \"node:pat"
  },
  {
    "path": "src/matches-where.test.ts",
    "chars": 2158,
    "preview": "import assert from 'node:assert/strict'\nimport test from 'node:test'\n\nimport type { JsonObject } from 'type-fest'\n\nimpor"
  },
  {
    "path": "src/matches-where.ts",
    "chars": 2870,
    "preview": "import type { JsonObject } from 'type-fest'\n\nimport { WHERE_OPERATORS, type WhereOperator } from './where-operators.ts'\n"
  },
  {
    "path": "src/paginate.test.ts",
    "chars": 2095,
    "preview": "import assert from 'node:assert/strict'\nimport test from 'node:test'\n\nimport { paginate } from './paginate.ts'\n\nawait te"
  },
  {
    "path": "src/paginate.ts",
    "chars": 989,
    "preview": "export type PaginationResult<T> = {\n  first: number\n  prev: number | null\n  next: number | null\n  last: number\n  pages: "
  },
  {
    "path": "src/parse-where.test.ts",
    "chars": 2207,
    "preview": "import assert from 'node:assert/strict'\nimport test from 'node:test'\n\nimport { parseWhere } from './parse-where.ts'\n\nawa"
  },
  {
    "path": "src/parse-where.ts",
    "chars": 1794,
    "preview": "import { setProperty } from 'dot-prop'\nimport type { JsonObject } from 'type-fest'\n\nimport { isWhereOperator, type Where"
  },
  {
    "path": "src/random-id.ts",
    "chars": 122,
    "preview": "import { randomBytes } from 'node:crypto'\n\nexport function randomId(): string {\n  return randomBytes(2).toString('hex')\n"
  },
  {
    "path": "src/service.test.ts",
    "chars": 5199,
    "preview": "import assert from 'node:assert/strict'\nimport test, { beforeEach } from 'node:test'\n\nimport { Low, Memory } from 'lowdb"
  },
  {
    "path": "src/service.ts",
    "chars": 6028,
    "preview": "import inflection from 'inflection'\nimport { Low } from 'lowdb'\nimport sortOn from 'sort-on'\nimport type { JsonObject } "
  },
  {
    "path": "src/where-operators.ts",
    "chars": 348,
    "preview": "export const WHERE_OPERATORS = [\n  'lt',\n  'lte',\n  'gt',\n  'gte',\n  'eq',\n  'ne',\n  'in',\n  'contains',\n  'startsWith',"
  },
  {
    "path": "tsconfig.json",
    "chars": 335,
    "preview": "{\n  \"exclude\": [\"src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"outDir\": \"./lib\",\n    \"target\": \"esnext\",\n    \"module\":"
  },
  {
    "path": "views/index.html",
    "chars": 3923,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  }
]

About this extraction

This page contains the full source code of the typicode/json-server GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 32 files (59.9 KB), approximately 17.9k tokens, and a symbol index with 68 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!