json-server 1.0.0-beta.0 → 1.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,44 +1,21 @@
1
- Fair Source License, version 0.9
2
-
3
- Copyright (C) 2023-present typicode
4
-
5
- Licensor: typicode
6
-
7
- Software: json-server
8
-
9
- Use Limitation: 2 users
10
-
11
- License Grant. Licensor hereby grants to each recipient of the
12
- Software ("you") a non-exclusive, non-transferable, royalty-free and
13
- fully-paid-up license, under all of the Licensor's copyright and
14
- patent rights, to use, copy, distribute, prepare derivative works of,
15
- publicly perform and display the Software, subject to the Use
16
- Limitation and the conditions set forth below.
17
-
18
- Use Limitation. The license granted above allows use by up to the
19
- number of users per entity set forth above (the "Use Limitation"). For
20
- determining the number of users, "you" includes all affiliates,
21
- meaning legal entities controlling, controlled by, or under common
22
- control with you. If you exceed the Use Limitation, your use is
23
- subject to payment of Licensor's then-current list price for licenses.
24
-
25
- Conditions. Redistribution in source code or other forms must include
26
- a copy of this license document to be provided in a reasonable
27
- manner. Any redistribution of the Software is only allowed subject to
28
- this license.
29
-
30
- Trademarks. This license does not grant you any right in the
31
- trademarks, service marks, brand names or logos of Licensor.
32
-
33
- DISCLAIMER. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OR
34
- CONDITION, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES
35
- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
36
- NONINFRINGEMENT. LICENSORS HEREBY DISCLAIM ALL LIABILITY, WHETHER IN
37
- AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
38
- CONNECTION WITH THE SOFTWARE.
39
-
40
- Termination. If you violate the terms of this license, your rights
41
- will terminate automatically and will not be reinstated without the
42
- prior written consent of Licensor. Any such termination will not
43
- affect the right of others who may have received copies of the
44
- Software from you.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 typicode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,11 +1,12 @@
1
- # json-server
1
+ # JSON-Server
2
2
 
3
3
  [![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)
4
4
 
5
5
  > [!IMPORTANT]
6
- > Viewing beta v1 documentation – usable but expect breaking changes. For stable version, see [here](https://github.com/typicode/json-server/tree/v0)
6
+ > Viewing beta v1 documentation – usable but expect breaking changes. For stable version, see [here](https://github.com/typicode/json-server/tree/v0.17.4)
7
7
 
8
- 👋 _Hey! Using React, Vue or Astro? Check my new project [MistCSS](https://github.com/typicode/mistcss) to write 50% less code._
8
+ > [!NOTE]
9
+ > Using React ⚛️ and tired of CSS-in-JS? See [MistCSS](https://github.com/typicode/mistcss) 👀
9
10
 
10
11
  ## Install
11
12
 
@@ -19,6 +20,7 @@ Create a `db.json` or `db.json5` file
19
20
 
20
21
  ```json
21
22
  {
23
+ "$schema": "./node_modules/json-server/schema.json",
22
24
  "posts": [
23
25
  { "id": "1", "title": "a title", "views": 100 },
24
26
  { "id": "2", "title": "another title", "views": 200 }
@@ -40,15 +42,15 @@ Create a `db.json` or `db.json5` file
40
42
  ```json5
41
43
  {
42
44
  posts: [
43
- { id: '1', title: 'a title', views: 100 },
44
- { id: '2', title: 'another title', views: 200 },
45
+ { id: "1", title: "a title", views: 100 },
46
+ { id: "2", title: "another title", views: 200 },
45
47
  ],
46
48
  comments: [
47
- { id: '1', text: 'a comment about post 1', postId: '1' },
48
- { id: '2', text: 'another comment about post 1', postId: '1' },
49
+ { id: "1", text: "a comment about post 1", postId: "1" },
50
+ { id: "2", text: "another comment about post 1", postId: "1" },
49
51
  ],
50
52
  profile: {
51
- name: 'typicode',
53
+ name: "typicode",
52
54
  },
53
55
  }
54
56
  ```
@@ -57,19 +59,30 @@ You can read more about JSON5 format [here](https://github.com/json5/json5).
57
59
 
58
60
  </details>
59
61
 
60
- Pass it to JSON Server CLI
62
+ Start JSON Server
61
63
 
62
- ```shell
63
- $ npx json-server db.json
64
+ ```bash
65
+ npx json-server db.json
64
66
  ```
65
67
 
66
- Get a REST API
68
+ This starts the server at `http://localhost:3000`. You should see:
69
+ ```
70
+ JSON Server started on PORT :3000
71
+ http://localhost:3000
72
+ ```
67
73
 
68
- ```shell
69
- $ curl http://localhost:3000/posts/1
74
+ Access your REST API:
75
+
76
+ ```bash
77
+ curl http://localhost:3000/posts/1
78
+ ```
79
+
80
+ **Response:**
81
+ ```json
70
82
  {
71
83
  "id": "1",
72
- "title": "a title"
84
+ "title": "a title",
85
+ "views": 100
73
86
  }
74
87
  ```
75
88
 
@@ -77,126 +90,174 @@ Run `json-server --help` for a list of options
77
90
 
78
91
  ## Sponsors ✨
79
92
 
80
- | Sponsors |
81
- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
82
- | <a href="https://mockend.com/" target="_blank"><img src="https://jsonplaceholder.typicode.com/mockend.svg" height="80px"></a> |
83
- | <a href="https://www.storyblok.com/" target="_blank"><img src="https://github.com/typicode/json-server/assets/5502029/c6b10674-4ada-4616-91b8-59d30046b45a" height="40px"></a> |
84
- | <a href="https://betterstack.com/" target="_blank"><img src="https://github.com/typicode/json-server/assets/5502029/44679f8f-9671-470d-b77e-26d90b90cbdc" height="40px"></a> |
85
- | <a href="https://www.globalsoftwarecompanies.com" target="_blank"><img src="https://github.com/typicode/json-server/assets/5502029/d0bb2e9b-9b74-4d91-9c9e-f3c87e152918" height="40px"></a> |
86
- | <a href="https://beeceptor.com/?utm_source=json-server"><img src="https://github.com/typicode/json-server/assets/5502029/57b852a6-60b9-426b-986e-a148e82783c2" height="40px"></a> |
93
+ ### Gold
94
+
95
+ | |
96
+ | :--------------------------------------------------------------------------------------------------------------------------------------------------------: |
97
+ | <a href="https://mockend.com/" target="_blank"><img src="https://jsonplaceholder.typicode.com/mockend.svg" height="100px"></a> |
98
+ | <a href="https://zuplo.link/json-server-gh"><img src="https://github.com/user-attachments/assets/adfee31f-a8b6-4684-9a9b-af4f03ac5b75" height="100px"></a> |
99
+ | <a href="https://www.mintlify.com/"><img src="https://github.com/user-attachments/assets/bcc8cc48-b2d9-4577-8939-1eb4196b7cc5" height="100px"></a> |
100
+ | <a href="http://git-tower.com/?utm_source=husky&utm_medium=referral"><img height="100px" alt="tower-dock-icon-light" src="https://github.com/user-attachments/assets/b6b4ab20-beff-4e5c-9845-bb9d60057196" /></a> |
101
+ | <a href="https://serpapi.com/?utm_source=typicode"><img height="100px" src="https://github.com/user-attachments/assets/52b3039d-1e4c-4c68-951c-93f0f1e73611" /></a>
102
+
103
+
104
+ ### Silver
105
+
106
+ | |
107
+ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
108
+ | <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> |
87
109
 
110
+ ### Bronze
111
+
112
+ | | |
113
+ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
114
+ | <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> |
88
115
 
89
116
  [Become a sponsor and have your company logo here](https://github.com/users/typicode/sponsorship)
90
117
 
91
- ## Sponsorware
118
+ ## Query Capabilities
92
119
 
93
- > [!NOTE]
94
- > This project uses the [Fair Source License](https://fair.io/). Only organizations with 3+ users are kindly asked to contribute a small amount through sponsorship [sponsor](https://github.com/sponsors/typicode) for usage. __This license helps keep the project sustainable and healthy, benefiting everyone.__
95
- >
96
- > For more information, FAQs, and the rationale behind this, visit [https://fair.io/](https://fair.io/).
120
+ JSON Server supports advanced querying out of the box:
121
+
122
+ ```http
123
+ GET /posts?views:gt=100 # Filter by condition
124
+ GET /posts?_sort=-views # Sort by field (descending)
125
+ GET /posts?_page=1&_per_page=10 # Pagination
126
+ GET /posts?_embed=comments # Include relations
127
+ GET /posts?_where={"or":[...]} # Complex queries
128
+ ```
129
+
130
+ See detailed documentation below for each feature.
97
131
 
98
132
  ## Routes
99
133
 
100
- Based on the example `db.json`, you'll get the following routes:
134
+ ### Array Resources
101
135
 
102
- ```
136
+ For array resources like `posts` and `comments`:
137
+
138
+ ```http
103
139
  GET /posts
104
140
  GET /posts/:id
105
141
  POST /posts
106
142
  PUT /posts/:id
107
143
  PATCH /posts/:id
108
144
  DELETE /posts/:id
109
-
110
- # Same for comments
111
145
  ```
112
146
 
113
- ```
147
+ ### Object Resources
148
+
149
+ For singular object resources like `profile`:
150
+
151
+ ```http
114
152
  GET /profile
115
153
  PUT /profile
116
154
  PATCH /profile
117
155
  ```
118
156
 
119
- ## Params
157
+ ## Query params
120
158
 
121
159
  ### Conditions
122
160
 
123
- - ` ` → `==`
124
- - `lt` → `<`
125
- - `lte` → `<=`
126
- - `gt` → `>`
127
- - `gte` → `>=`
128
- - `ne` `!=`
129
-
161
+ Use `field:operator=value`.
162
+
163
+ Operators:
164
+
165
+ - no operator -> `eq` (equal)
166
+ - `lt` less than, `lte` less than or equal
167
+ - `gt` greater than, `gte` greater than or equal
168
+ - `eq` equal, `ne` not equal
169
+ - `in` included in comma-separated list
170
+ - `contains` string contains (case-insensitive)
171
+ - `startsWith` string starts with (case-insensitive)
172
+ - `endsWith` string ends with (case-insensitive)
173
+
174
+ Examples:
175
+
176
+ ```http
177
+ GET /posts?views:gt=100
178
+ GET /posts?title:eq=Hello
179
+ GET /posts?id:in=1,2,3
180
+ GET /posts?author.name:eq=typicode
181
+ GET /posts?title:contains=hello
182
+ GET /posts?title:startsWith=Hello
183
+ GET /posts?title:endsWith=world
130
184
  ```
131
- GET /posts?views_gt=9000
132
- ```
133
-
134
- ### Range
135
185
 
136
- - `start`
137
- - `end`
138
- - `limit`
186
+ ### Sort
139
187
 
188
+ ```http
189
+ GET /posts?_sort=title
190
+ GET /posts?_sort=-views
191
+ GET /posts?_sort=author.name,-views
140
192
  ```
141
- GET /posts?_start=10&_end=20
142
- GET /posts?_start=10&_limit=10
143
- ```
144
-
145
- ### Paginate
146
193
 
147
- - `page`
148
- - `per_page` (default = 10)
194
+ ### Pagination
149
195
 
150
- ```
196
+ ```http
151
197
  GET /posts?_page=1&_per_page=25
152
198
  ```
153
199
 
154
- ### Sort
155
-
156
- - `_sort=f1,f2`
157
-
158
- ```
159
- GET /posts?_sort=id,-views
200
+ **Response:**
201
+ ```json
202
+ {
203
+ "first": 1,
204
+ "prev": null,
205
+ "next": 2,
206
+ "last": 4,
207
+ "pages": 4,
208
+ "items": 100,
209
+ "data": [
210
+ { "id": "1", "title": "...", "views": 100 },
211
+ { "id": "2", "title": "...", "views": 200 }
212
+ ]
213
+ }
160
214
  ```
161
215
 
162
- ### Nested and array fields
163
-
164
- - `x.y.z...`
165
- - `x.y.z[i]...`
166
-
167
- ```
168
- GET /foo?a.b=bar
169
- GET /foo?x.y_lt=100
170
- GET /foo?arr[0]=bar
171
- ```
216
+ **Notes:**
217
+ - `_per_page` defaults to `10` if not specified
218
+ - Invalid `_page` or `_per_page` values are automatically normalized to valid ranges
172
219
 
173
220
  ### Embed
174
221
 
175
- ```
222
+ ```http
176
223
  GET /posts?_embed=comments
177
224
  GET /comments?_embed=post
178
225
  ```
179
226
 
180
- ## Delete
227
+ ### Complex filter with `_where`
181
228
 
229
+ `_where` accepts a JSON object and overrides normal query params when valid.
230
+
231
+ ```http
232
+ GET /posts?_where={"or":[{"views":{"gt":100}},{"author":{"name":{"lt":"m"}}}]}
182
233
  ```
183
- DELETE /posts/1
234
+
235
+ ## Delete dependents
236
+
237
+ ```http
184
238
  DELETE /posts/1?_dependent=comments
185
239
  ```
186
240
 
187
- ## Serving static files
241
+ ## Static Files
188
242
 
189
- If you create a `./public` directory, JSON Serve will serve its content in addition to the REST API.
243
+ JSON Server automatically serves files from the `./public` directory.
190
244
 
191
- You can also add custom directories using `-s/--static` option.
245
+ To serve additional static directories:
192
246
 
193
- ```sh
194
- json-server -s ./static
195
- json-server -s ./static -s ./node_modules
247
+ ```bash
248
+ json-server db.json -s ./static
249
+ json-server db.json -s ./static -s ./node_modules
196
250
  ```
197
251
 
198
- ## Notable differences with v0.17
252
+ Static files are served with standard MIME types and can include HTML, CSS, JavaScript, images, and other assets.
253
+
254
+ ## Migration Notes (v0 → v1)
255
+
256
+ If you are upgrading from json-server v0.x, note these behavioral changes:
257
+
258
+ - **ID handling:** `id` is always a string and will be auto-generated if not provided
259
+ - **Pagination:** Use `_per_page` with `_page` instead of the deprecated `_limit` parameter
260
+ - **Relationships:** Use `_embed` instead of `_expand` for including related resources
261
+ - **Request delays:** Use browser DevTools (Network tab > throttling) instead of the removed `--delay` CLI option
199
262
 
200
- - `id` is always a string and will be generated for you if missing
201
- - use `_per_page` with `_page` instead of `_limit`for pagination
202
- - use Chrome's `Network tab > throtling` to delay requests instead of `--delay` CLI option
263
+ > **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.
package/lib/app.js CHANGED
@@ -3,15 +3,70 @@ import { fileURLToPath } from 'node:url';
3
3
  import { App } from '@tinyhttp/app';
4
4
  import { cors } from '@tinyhttp/cors';
5
5
  import { Eta } from 'eta';
6
+ import { Low } from 'lowdb';
6
7
  import { json } from 'milliparsec';
7
8
  import sirv from 'sirv';
8
- import { isItem, Service } from './service.js';
9
+ import { parseWhere } from "./parse-where.js";
10
+ import { isItem, Service } from "./service.js";
9
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
12
  const isProduction = process.env['NODE_ENV'] === 'production';
11
13
  const eta = new Eta({
12
14
  views: join(__dirname, '../views'),
13
15
  cache: isProduction,
14
16
  });
17
+ const RESERVED_QUERY_KEYS = new Set(['_sort', '_page', '_per_page', '_embed', '_where']);
18
+ function parseListParams(req) {
19
+ const queryString = req.url.split('?')[1] ?? '';
20
+ const params = new URLSearchParams(queryString);
21
+ const filterParams = new URLSearchParams();
22
+ for (const [key, value] of params.entries()) {
23
+ if (!RESERVED_QUERY_KEYS.has(key)) {
24
+ filterParams.append(key, value);
25
+ }
26
+ }
27
+ let where = parseWhere(filterParams.toString());
28
+ const rawWhere = params.get('_where');
29
+ if (typeof rawWhere === 'string') {
30
+ try {
31
+ const parsed = JSON.parse(rawWhere);
32
+ if (typeof parsed === 'object' && parsed !== null) {
33
+ where = parsed;
34
+ }
35
+ }
36
+ catch {
37
+ // Ignore invalid JSON and fallback to parsed query params
38
+ }
39
+ }
40
+ const pageRaw = params.get('_page');
41
+ const perPageRaw = params.get('_per_page');
42
+ const page = pageRaw === null ? undefined : Number.parseInt(pageRaw, 10);
43
+ const perPage = perPageRaw === null ? undefined : Number.parseInt(perPageRaw, 10);
44
+ return {
45
+ where,
46
+ sort: params.get('_sort') ?? undefined,
47
+ page: Number.isNaN(page) ? undefined : page,
48
+ perPage: Number.isNaN(perPage) ? undefined : perPage,
49
+ embed: req.query['_embed'],
50
+ };
51
+ }
52
+ function withBody(action) {
53
+ return async (req, res, next) => {
54
+ const { name = '' } = req.params;
55
+ if (isItem(req.body)) {
56
+ res.locals['data'] = await action(name, req.body);
57
+ }
58
+ next?.();
59
+ };
60
+ }
61
+ function withIdAndBody(action) {
62
+ return async (req, res, next) => {
63
+ const { name = '', id = '' } = req.params;
64
+ if (isItem(req.body)) {
65
+ res.locals['data'] = await action(name, id, req.body);
66
+ }
67
+ next?.();
68
+ };
69
+ }
15
70
  export function createApp(db, options = {}) {
16
71
  // Create service
17
72
  const service = new Service(db);
@@ -23,69 +78,44 @@ export function createApp(db, options = {}) {
23
78
  ?.map((path) => (isAbsolute(path) ? path : join(process.cwd(), path)))
24
79
  .forEach((dir) => app.use(sirv(dir, { dev: !isProduction })));
25
80
  // CORS
26
- app.use(cors()).options('*', cors());
81
+ app
82
+ .use((req, res, next) => {
83
+ return cors({
84
+ allowedHeaders: req.headers['access-control-request-headers']
85
+ ?.split(',')
86
+ .map((h) => h.trim()),
87
+ })(req, res, next);
88
+ })
89
+ .options('*', cors());
27
90
  // Body parser
28
91
  app.use(json());
29
92
  app.get('/', (_req, res) => res.send(eta.render('index.html', { data: db.data })));
30
93
  app.get('/:name', (req, res, next) => {
31
94
  const { name = '' } = req.params;
32
- const query = Object.fromEntries(Object.entries(req.query)
33
- .map(([key, value]) => {
34
- if (['_start', '_end', '_limit', '_page', '_per_page'].includes(key) && typeof value === 'string') {
35
- return [key, parseInt(value)];
36
- }
37
- else {
38
- return [key, value];
39
- }
40
- })
41
- .filter(([_, value]) => !Number.isNaN(value)));
42
- res.locals['data'] = service.find(name, query);
43
- next();
95
+ const { where, sort, page, perPage, embed } = parseListParams(req);
96
+ res.locals['data'] = service.find(name, {
97
+ where,
98
+ sort,
99
+ page,
100
+ perPage,
101
+ embed,
102
+ });
103
+ next?.();
44
104
  });
45
105
  app.get('/:name/:id', (req, res, next) => {
46
106
  const { name = '', id = '' } = req.params;
47
107
  res.locals['data'] = service.findById(name, id, req.query);
48
- next();
49
- });
50
- app.post('/:name', async (req, res, next) => {
51
- const { name = '' } = req.params;
52
- if (isItem(req.body)) {
53
- res.locals['data'] = await service.create(name, req.body);
54
- }
55
- next();
56
- });
57
- app.put('/:name', async (req, res, next) => {
58
- const { name = '' } = req.params;
59
- if (isItem(req.body)) {
60
- res.locals['data'] = await service.update(name, req.body);
61
- }
62
- next();
63
- });
64
- app.put('/:name/:id', async (req, res, next) => {
65
- const { name = '', id = '' } = req.params;
66
- if (isItem(req.body)) {
67
- res.locals['data'] = await service.updateById(name, id, req.body);
68
- }
69
- next();
70
- });
71
- app.patch('/:name', async (req, res, next) => {
72
- const { name = '' } = req.params;
73
- if (isItem(req.body)) {
74
- res.locals['data'] = await service.patch(name, req.body);
75
- }
76
- next();
77
- });
78
- app.patch('/:name/:id', async (req, res, next) => {
79
- const { name = '', id = '' } = req.params;
80
- if (isItem(req.body)) {
81
- res.locals['data'] = await service.patchById(name, id, req.body);
82
- }
83
- next();
108
+ next?.();
84
109
  });
110
+ app.post('/:name', withBody(service.create.bind(service)));
111
+ app.put('/:name', withBody(service.update.bind(service)));
112
+ app.put('/:name/:id', withIdAndBody(service.updateById.bind(service)));
113
+ app.patch('/:name', withBody(service.patch.bind(service)));
114
+ app.patch('/:name/:id', withIdAndBody(service.patchById.bind(service)));
85
115
  app.delete('/:name/:id', async (req, res, next) => {
86
116
  const { name = '', id = '' } = req.params;
87
- res.locals['data'] = await service.destroyById(name, id, req.query['dependent']);
88
- next();
117
+ res.locals['data'] = await service.destroyById(name, id, req.query['_dependent']);
118
+ next?.();
89
119
  });
90
120
  app.use('/:name', (req, res) => {
91
121
  const { data } = res.locals;