@yrest/cli 0.5.3
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/README.md +819 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1514 -0
- package/dist/cli/index.mjs +1487 -0
- package/dist/index.d.mts +257 -0
- package/dist/index.d.ts +257 -0
- package/dist/index.js +1179 -0
- package/dist/index.mjs +1140 -0
- package/package.json +90 -0
package/README.md
ADDED
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
# yRest
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@yrest/cli)
|
|
4
|
+
[](https://www.npmjs.com/package/@yrest/cli)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/aggiovato/yRest/actions)
|
|
7
|
+
[](https://www.npmjs.com/package/@yrest/cli)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
|
|
10
|
+
YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a `db.yml` file.
|
|
11
|
+
|
|
12
|
+
> Think `json-server`, but powered by YAML — with relations, filters, pagination, nested routes, snapshots and custom handlers.
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
# db.yml
|
|
16
|
+
users:
|
|
17
|
+
- id: 1
|
|
18
|
+
name: Ana
|
|
19
|
+
email: ana@test.com
|
|
20
|
+
|
|
21
|
+
posts:
|
|
22
|
+
- id: 1
|
|
23
|
+
title: First post
|
|
24
|
+
userId: 1
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx @yrest/cli serve db.yml
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
GET /users → [{ id: 1, name: "Ana", email: "ana@test.com" }]
|
|
33
|
+
GET /users/1 → { id: 1, name: "Ana", email: "ana@test.com" }
|
|
34
|
+
POST /users → 201 Created
|
|
35
|
+
PUT /users/1 → 200 OK
|
|
36
|
+
PATCH /users/1 → 200 OK
|
|
37
|
+
DELETE /users/1 → 200 OK
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Why yRest?
|
|
43
|
+
|
|
44
|
+
A YAML-first alternative to json-server for frontend development.
|
|
45
|
+
|
|
46
|
+
| Feature | yrest | json-server |
|
|
47
|
+
| -------------------------------------------- | :---: | :---------: |
|
|
48
|
+
| YAML database | ✅ | ❌ |
|
|
49
|
+
| Zero config | ✅ | ✅ |
|
|
50
|
+
| Full CRUD | ✅ | ✅ |
|
|
51
|
+
| Field operators (`_gte`, `_like`, `_regex`…) | ✅ | ⚠️ |
|
|
52
|
+
| Full-text search | ✅ | ✅ |
|
|
53
|
+
| Relations + nested routes | ✅ | ✅ |
|
|
54
|
+
| Field projection (`_fields`) | ✅ | ❌ |
|
|
55
|
+
| Pageable mode (envelope response) | ✅ | ❌ |
|
|
56
|
+
| Custom static routes (`_routes`) | ✅ | ❌ |
|
|
57
|
+
| Template variables in responses | ✅ | ❌ |
|
|
58
|
+
| Handler functions (JS logic) | ✅ | ❌ |
|
|
59
|
+
| Snapshot endpoints | ✅ | ❌ |
|
|
60
|
+
| Config file | ✅ | ⚠️ |
|
|
61
|
+
| API overview page (`/_about`) | ✅ | ❌ |
|
|
62
|
+
| Watch mode | ✅ | ✅ |
|
|
63
|
+
| Readonly mode | ✅ | ❌ |
|
|
64
|
+
| Atomic writes | ✅ | ✅ |
|
|
65
|
+
| TypeScript types | ✅ | ❌ |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install -D @yrest/cli
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or run directly with npx (no install needed):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx @yrest/cli serve db.yml
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Quick start
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Create a sample db.yml and yrest.config.yml
|
|
87
|
+
npx @yrest/cli init
|
|
88
|
+
|
|
89
|
+
# Start the server
|
|
90
|
+
npx @yrest/cli serve db.yml
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
yrest · http://localhost:3070
|
|
95
|
+
|
|
96
|
+
Collections (base: /):
|
|
97
|
+
CRUD /users
|
|
98
|
+
CRUD /posts
|
|
99
|
+
|
|
100
|
+
Meta:
|
|
101
|
+
GET /_about
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Open `http://localhost:3070/_about` for a live overview of all generated endpoints, active modes and ready-to-run `curl` examples.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Commands
|
|
109
|
+
|
|
110
|
+
### `init`
|
|
111
|
+
|
|
112
|
+
Creates a sample `db.yml` and a `yrest.config.yml` template in the current directory.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npx @yrest/cli init # basic sample (default)
|
|
116
|
+
npx @yrest/cli init --sample relational # with _rel relations
|
|
117
|
+
npx @yrest/cli init --file api.yml # custom filename
|
|
118
|
+
npx @yrest/cli init --sample relational --file api.yml
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
| Flag | Default | Description |
|
|
122
|
+
| ---------- | -------- | ----------------------------------- |
|
|
123
|
+
| `--file` | `db.yml` | Output filename |
|
|
124
|
+
| `--sample` | `basic` | Sample data (`basic`, `relational`) |
|
|
125
|
+
|
|
126
|
+
**Samples:**
|
|
127
|
+
|
|
128
|
+
- `basic` — two independent collections: `users` and `products`
|
|
129
|
+
- `relational` — three collections with `_rel` relationships: `users`, `posts` and `comments`
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### `serve`
|
|
134
|
+
|
|
135
|
+
Starts the mock server.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npx @yrest/cli serve db.yml
|
|
139
|
+
npx @yrest/cli serve db.yml --port 3001 --host 0.0.0.0
|
|
140
|
+
npx @yrest/cli serve db.yml --base /api --watch
|
|
141
|
+
npx @yrest/cli serve db.yml --readonly --delay 300
|
|
142
|
+
npx @yrest/cli serve db.yml --pageable 20
|
|
143
|
+
npx @yrest/cli serve db.yml --snapshot
|
|
144
|
+
npx @yrest/cli serve db.yml --handlers yrest.handlers.js
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
| Flag | Default | Description |
|
|
148
|
+
| ------------------- | ----------- | ----------------------------------------------------------------------- |
|
|
149
|
+
| `--port` | `3070` | Port to listen on |
|
|
150
|
+
| `--host` | `localhost` | Host to bind |
|
|
151
|
+
| `--base` | _(none)_ | Prefix for all routes (e.g. `/api`) |
|
|
152
|
+
| `--watch` | `false` | Reload `db.yml` automatically when it changes on disk |
|
|
153
|
+
| `--readonly` | `false` | Reject all write operations (POST, PUT, PATCH, DELETE) with `405` |
|
|
154
|
+
| `--delay <ms>` | `0` | Add a fixed delay to all responses (simulates network latency) |
|
|
155
|
+
| `--pageable [n]` | `false` | Wrap GET collection responses in `{ data, pagination }`. Optional limit |
|
|
156
|
+
| `--snapshot` | `false` | Save initial state snapshot and expose `/_snapshot` endpoints |
|
|
157
|
+
| `--handlers <file>` | _(none)_ | Path to a JS file exporting handler functions for custom routes |
|
|
158
|
+
|
|
159
|
+
All flags can also be set in `yrest.config.yml` (see below). CLI flags always take priority over the config file.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Configuration file
|
|
164
|
+
|
|
165
|
+
`yrest init` creates a `yrest.config.yml` alongside `db.yml`. Options defined here apply every time you run `serve` without needing to type flags:
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
# yrest.config.yml
|
|
169
|
+
file: db.yml
|
|
170
|
+
port: 3070
|
|
171
|
+
host: localhost
|
|
172
|
+
# base: /api
|
|
173
|
+
# watch: false
|
|
174
|
+
# readonly: false
|
|
175
|
+
# delay: 0
|
|
176
|
+
# pageable: false # true (limit 10), or a number (custom limit)
|
|
177
|
+
# snapshot: false
|
|
178
|
+
# handlers: yrest.handlers.js
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Priority order** (highest wins): CLI flags → `yrest.config.yml` → schema defaults.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Database format
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
users:
|
|
189
|
+
- id: 1
|
|
190
|
+
name: Ana
|
|
191
|
+
email: ana@test.com
|
|
192
|
+
- id: 2
|
|
193
|
+
name: Luis
|
|
194
|
+
email: luis@test.com
|
|
195
|
+
|
|
196
|
+
posts:
|
|
197
|
+
- id: 1
|
|
198
|
+
title: First post
|
|
199
|
+
userId: 1
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Each top-level key becomes a resource with full CRUD endpoints.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Generated endpoints
|
|
207
|
+
|
|
208
|
+
For each resource in `db.yml`:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
GET /users List all
|
|
212
|
+
GET /users/:id Get one
|
|
213
|
+
POST /users Create
|
|
214
|
+
PUT /users/:id Replace
|
|
215
|
+
PATCH /users/:id Partial update
|
|
216
|
+
DELETE /users/:id Delete
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
With `--base /api` all routes are prefixed: `/api/users`, `/api/users/:id`, etc.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Query params
|
|
224
|
+
|
|
225
|
+
All query params can be combined freely.
|
|
226
|
+
|
|
227
|
+
### Filtering
|
|
228
|
+
|
|
229
|
+
Return only items that match one or more field values:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
GET /users?name=Ana
|
|
233
|
+
GET /users?role=admin&active=true
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Comparison is case-sensitive and converts types to string (`?id=1` matches numeric `id: 1`).
|
|
237
|
+
|
|
238
|
+
Repeated params are treated as OR — any match passes:
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
GET /users?role=admin&role=editor # returns admins and editors
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Field operators
|
|
245
|
+
|
|
246
|
+
Append an operator suffix to any field name:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
GET /users?age_gte=18 # age >= 18
|
|
250
|
+
GET /users?age_lte=65 # age <= 65
|
|
251
|
+
GET /users?status_ne=inactive # status != "inactive"
|
|
252
|
+
GET /users?name_like=ana # name contains "ana" (case-insensitive)
|
|
253
|
+
GET /users?name_start=A # name starts with "A" (case-insensitive)
|
|
254
|
+
GET /users?email_regex=@gmail\.com # email matches regex (case-insensitive)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
| Suffix | Type | Description |
|
|
258
|
+
| -------- | ---------------- | -------------------------------- |
|
|
259
|
+
| `_gte` | numeric / string | Greater than or equal |
|
|
260
|
+
| `_lte` | numeric / string | Less than or equal |
|
|
261
|
+
| `_ne` | any | Not equal |
|
|
262
|
+
| `_like` | string | Case-insensitive substring match |
|
|
263
|
+
| `_start` | string | Case-insensitive prefix match |
|
|
264
|
+
| `_regex` | string | Case-insensitive regex match |
|
|
265
|
+
|
|
266
|
+
### Full-text search
|
|
267
|
+
|
|
268
|
+
Search across all scalar fields of every item (case-insensitive substring match):
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
GET /users?_q=ana
|
|
272
|
+
GET /posts?_q=javascript
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
An item passes if any string or number field contains the search term.
|
|
276
|
+
|
|
277
|
+
### Sorting
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
GET /users?_sort=name # ascending (default)
|
|
281
|
+
GET /users?_sort=name&_order=desc # descending
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
String fields are compared case-insensitively. Items missing the sort field are pushed to the end.
|
|
285
|
+
|
|
286
|
+
### Pagination
|
|
287
|
+
|
|
288
|
+
**Without `--pageable`** (default):
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
GET /users?_page=1&_limit=10 # page 1, 10 items per page
|
|
292
|
+
GET /users?_limit=5 # first 5 items
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
When `_page` or `_limit` are used, the response includes an `X-Total-Count` header with the total number of items before pagination.
|
|
296
|
+
|
|
297
|
+
**With `--pageable`** (or `pageable: true` in config):
|
|
298
|
+
|
|
299
|
+
Every GET collection response is automatically wrapped in a `{ data, pagination }` envelope:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
npx @yrest/cli serve db.yml --pageable # default limit: 10
|
|
303
|
+
npx @yrest/cli serve db.yml --pageable 20 # custom limit: 20
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```json
|
|
307
|
+
{
|
|
308
|
+
"data": [
|
|
309
|
+
{ "id": 1, "name": "Ana" },
|
|
310
|
+
{ "id": 2, "name": "Luis" }
|
|
311
|
+
],
|
|
312
|
+
"pagination": {
|
|
313
|
+
"page": 1,
|
|
314
|
+
"limit": 10,
|
|
315
|
+
"totalItems": 23,
|
|
316
|
+
"totalPages": 3,
|
|
317
|
+
"isFirst": true,
|
|
318
|
+
"isLast": false,
|
|
319
|
+
"hasNext": true,
|
|
320
|
+
"hasPrev": false
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The `?_page` and `?_limit` query params still work in pageable mode to navigate pages.
|
|
326
|
+
|
|
327
|
+
### Field projection
|
|
328
|
+
|
|
329
|
+
Return only specific fields in the response:
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
GET /users?_fields=id,name
|
|
333
|
+
GET /posts?_fields=id,title,userId
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Works on both collection and single-item endpoints.
|
|
337
|
+
|
|
338
|
+
### Relation embedding (`_expand`)
|
|
339
|
+
|
|
340
|
+
Embed a related parent object directly into the response using the `_rel` block (see [Relational data](#relational-data)):
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
GET /posts?_expand=user # embed user object in each post
|
|
344
|
+
GET /posts/1?_expand=user # embed in a single item
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Both syntaxes are supported:
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
?_expand=author,category # comma-separated
|
|
351
|
+
?_expand=author&_expand=category # repeated param
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Unresolvable keys are silently ignored. Works on all operations: GET, POST, PUT, PATCH, DELETE.
|
|
355
|
+
|
|
356
|
+
### Embed children (`_embed`)
|
|
357
|
+
|
|
358
|
+
Embed related child collections directly into a parent item:
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
GET /users/1?_embed=posts # embed all posts where userId === 1
|
|
362
|
+
GET /users?_embed=posts # embed posts in every user
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Both syntaxes are supported:
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
?_embed=posts,comments # comma-separated
|
|
369
|
+
?_embed=posts&_embed=comments # repeated param
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Requires `_rel` to be declared (see [Relational data](#relational-data)).
|
|
373
|
+
|
|
374
|
+
### Combined example
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
GET /posts?userId=1&_sort=title&_order=asc&_page=1&_limit=5&_expand=user&_fields=id,title,user
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Returns the first 5 posts by user 1, sorted alphabetically by title, with the user object embedded, returning only `id`, `title` and `user` fields.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Relational data
|
|
385
|
+
|
|
386
|
+
Use `_rel` to declare foreign key relationships between collections:
|
|
387
|
+
|
|
388
|
+
```yaml
|
|
389
|
+
_rel:
|
|
390
|
+
posts:
|
|
391
|
+
userId: users
|
|
392
|
+
|
|
393
|
+
users:
|
|
394
|
+
- id: 1
|
|
395
|
+
name: Ana
|
|
396
|
+
|
|
397
|
+
posts:
|
|
398
|
+
- id: 1
|
|
399
|
+
title: First post
|
|
400
|
+
userId: 1
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
This enables:
|
|
404
|
+
|
|
405
|
+
**Nested routes:**
|
|
406
|
+
|
|
407
|
+
```
|
|
408
|
+
GET /users/1/posts # all posts where userId === 1
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**Embed parent with `?_expand`:**
|
|
412
|
+
|
|
413
|
+
```
|
|
414
|
+
GET /posts/1?_expand=user → { id: 1, title: "First post", userId: 1, user: { id: 1, name: "Ana" } }
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Embed children with `?_embed`:**
|
|
418
|
+
|
|
419
|
+
```
|
|
420
|
+
GET /users/1?_embed=posts → { id: 1, name: "Ana", posts: [{ id: 1, title: "First post", userId: 1 }] }
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Custom routes
|
|
426
|
+
|
|
427
|
+
Define endpoints that don't fit CRUD directly in `db.yml` using the `_routes` block.
|
|
428
|
+
|
|
429
|
+
### Static responses
|
|
430
|
+
|
|
431
|
+
```yaml
|
|
432
|
+
_routes:
|
|
433
|
+
- method: POST
|
|
434
|
+
path: /login
|
|
435
|
+
response:
|
|
436
|
+
status: 200
|
|
437
|
+
body:
|
|
438
|
+
token: fake-jwt-token-abc123
|
|
439
|
+
|
|
440
|
+
- method: POST
|
|
441
|
+
path: /logout
|
|
442
|
+
response:
|
|
443
|
+
status: 204
|
|
444
|
+
|
|
445
|
+
- method: GET
|
|
446
|
+
path: /dashboard/stats
|
|
447
|
+
response:
|
|
448
|
+
status: 200
|
|
449
|
+
headers:
|
|
450
|
+
Cache-Control: no-store
|
|
451
|
+
body:
|
|
452
|
+
users: 150
|
|
453
|
+
revenue: 4820.50
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
- `method` is case-insensitive. `path` supports Fastify params (`:id`).
|
|
457
|
+
- `response.status` defaults to `200`. `response.body` is any YAML value.
|
|
458
|
+
- Custom routes are registered before resource routes and always take priority.
|
|
459
|
+
- Shown in `/_about` under "Custom routes".
|
|
460
|
+
|
|
461
|
+
### Template variables
|
|
462
|
+
|
|
463
|
+
Interpolate request data into the response body using `{{}}` syntax:
|
|
464
|
+
|
|
465
|
+
```yaml
|
|
466
|
+
_routes:
|
|
467
|
+
- method: GET
|
|
468
|
+
path: /users/:id/summary
|
|
469
|
+
response:
|
|
470
|
+
status: 200
|
|
471
|
+
body:
|
|
472
|
+
requestedId: "{{params.id}}"
|
|
473
|
+
timestamp: "{{now}}"
|
|
474
|
+
|
|
475
|
+
- method: POST
|
|
476
|
+
path: /echo
|
|
477
|
+
response:
|
|
478
|
+
status: 200
|
|
479
|
+
body:
|
|
480
|
+
received: "{{body}}"
|
|
481
|
+
query: "{{query}}"
|
|
482
|
+
requestId: "{{uuid}}"
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Available variables:
|
|
486
|
+
|
|
487
|
+
| Variable | Description |
|
|
488
|
+
| --------------- | --------------------------------------------------- |
|
|
489
|
+
| `{{params.X}}` | URL parameter (e.g. `{{params.id}}`) |
|
|
490
|
+
| `{{query.X}}` | Query string param (e.g. `{{query.page}}`) |
|
|
491
|
+
| `{{body}}` | Full request body |
|
|
492
|
+
| `{{body.X}}` | Field from the request body (e.g. `{{body.email}}`) |
|
|
493
|
+
| `{{headers.X}}` | Request header value |
|
|
494
|
+
| `{{now}}` | Current UTC timestamp (ISO 8601) |
|
|
495
|
+
| `{{uuid}}` | Random UUID v4 |
|
|
496
|
+
|
|
497
|
+
When a field contains only a single `{{variable}}` placeholder, the resolved value preserves its original type (number, boolean, object). When embedded in a larger string it is stringified.
|
|
498
|
+
|
|
499
|
+
### Handler functions
|
|
500
|
+
|
|
501
|
+
For routes that need real logic (conditional responses, stateful mocks, request inspection), reference a JavaScript function via the `handler:` field:
|
|
502
|
+
|
|
503
|
+
```yaml
|
|
504
|
+
_routes:
|
|
505
|
+
- method: POST
|
|
506
|
+
path: /login
|
|
507
|
+
handler: login
|
|
508
|
+
response: # optional fallback if handler throws
|
|
509
|
+
status: 200
|
|
510
|
+
body: { token: fake }
|
|
511
|
+
|
|
512
|
+
- method: GET
|
|
513
|
+
path: /auth/me
|
|
514
|
+
handler: getCurrentUser
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Create a `yrest.handlers.js` file in the same directory as `db.yml`:
|
|
518
|
+
|
|
519
|
+
```js
|
|
520
|
+
// yrest.handlers.js
|
|
521
|
+
export async function login(req) {
|
|
522
|
+
const { email, password } = req.body ?? {};
|
|
523
|
+
if (password !== "secret") return { status: 401, body: { error: "Invalid credentials" } };
|
|
524
|
+
return { status: 200, body: { token: `tok-${email}` } };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export async function getCurrentUser(req) {
|
|
528
|
+
return { status: 200, body: { id: 1, name: "Ana", role: "admin" } };
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Pass the file to the server with `--handlers`:
|
|
533
|
+
|
|
534
|
+
```bash
|
|
535
|
+
npx @yrest/cli serve db.yml --handlers yrest.handlers.js
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**Handler signature:**
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
type HandlerRequest = {
|
|
542
|
+
params: Record<string, string>;
|
|
543
|
+
query: Record<string, string | string[]>;
|
|
544
|
+
body: unknown;
|
|
545
|
+
headers: Record<string, string | string[]>;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
type HandlerResponse = {
|
|
549
|
+
status?: number; // defaults to 200
|
|
550
|
+
body?: unknown;
|
|
551
|
+
headers?: Record<string, string>;
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
type Handler = (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>;
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
If a named handler is not found in the file, the server returns `501`. If the handler throws, it returns `500`. If a `response:` block is defined alongside `handler:`, it is used as fallback only when the handler itself throws.
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## Server modes
|
|
562
|
+
|
|
563
|
+
### Watch mode
|
|
564
|
+
|
|
565
|
+
Automatically reloads `db.yml` when it changes on disk — useful when you edit the file manually while the server is running:
|
|
566
|
+
|
|
567
|
+
```bash
|
|
568
|
+
npx @yrest/cli serve db.yml --watch
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
> **Note:** Watch mode reloads data in existing collections. Adding or removing entire collections requires a server restart.
|
|
572
|
+
|
|
573
|
+
### Readonly mode
|
|
574
|
+
|
|
575
|
+
Rejects all write operations with `405 Method Not Allowed`:
|
|
576
|
+
|
|
577
|
+
```bash
|
|
578
|
+
npx @yrest/cli serve db.yml --readonly
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
Useful to expose a stable read-only snapshot for demos or CI environments.
|
|
582
|
+
|
|
583
|
+
### Delay mode
|
|
584
|
+
|
|
585
|
+
Adds a fixed delay (in milliseconds) to every response to simulate real network latency:
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
npx @yrest/cli serve db.yml --delay 500 # 500ms on every response
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Snapshot mode
|
|
592
|
+
|
|
593
|
+
Saves the initial database state at startup and exposes three meta endpoints to inspect, save and restore it:
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
npx @yrest/cli serve db.yml --snapshot
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
| Endpoint | Description |
|
|
600
|
+
| ----------------------- | ------------------------------------------------------------------- |
|
|
601
|
+
| `GET /_snapshot` | Returns snapshot metadata (saved time + item counts per collection) |
|
|
602
|
+
| `POST /_snapshot/save` | Replaces the snapshot with the current database state |
|
|
603
|
+
| `POST /_snapshot/reset` | Restores the database to the last saved snapshot |
|
|
604
|
+
|
|
605
|
+
Useful for test suites that need a clean reset between runs or demos that need a predictable starting state.
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## API overview page
|
|
610
|
+
|
|
611
|
+
Every running server exposes `GET /_about` — a self-contained HTML page listing all generated endpoints, custom routes, active modes, query param reference and ready-to-run `curl` examples derived from your actual `db.yml`:
|
|
612
|
+
|
|
613
|
+
```bash
|
|
614
|
+
open http://localhost:3070/_about
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
The page reflects the live state of the server, so it updates automatically in watch mode.
|
|
618
|
+
|
|
619
|
+
---
|
|
620
|
+
|
|
621
|
+
## HTTP responses
|
|
622
|
+
|
|
623
|
+
| Status | When |
|
|
624
|
+
| ------ | -------------------------------------------------------------------- |
|
|
625
|
+
| `200` | Successful GET, PUT, PATCH, DELETE |
|
|
626
|
+
| `201` | Successful POST |
|
|
627
|
+
| `400` | Invalid or missing request body |
|
|
628
|
+
| `404` | Resource or id not found |
|
|
629
|
+
| `405` | Write operation in readonly mode |
|
|
630
|
+
| `500` | Error reading or writing the YAML file |
|
|
631
|
+
| `501` | Handler referenced in `_routes` is not exported by the handlers file |
|
|
632
|
+
|
|
633
|
+
DELETE returns the deleted item as confirmation.
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## ID generation
|
|
638
|
+
|
|
639
|
+
If a POST body does not include an `id`, yrest assigns the next incremental integer automatically. If the body includes an `id`, it is respected.
|
|
640
|
+
|
|
641
|
+
## Persistence
|
|
642
|
+
|
|
643
|
+
All write operations (POST, PUT, PATCH, DELETE) are saved back to `db.yml` immediately using an atomic write strategy (write to temp file → rename), so data is never corrupted even if the process is interrupted.
|
|
644
|
+
|
|
645
|
+
## CORS
|
|
646
|
+
|
|
647
|
+
CORS is enabled by default, so you can call the API from any frontend running on a different port without extra configuration.
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## Frontend usage
|
|
652
|
+
|
|
653
|
+
```ts
|
|
654
|
+
// List all
|
|
655
|
+
const users = await fetch("http://localhost:3070/users").then((r) => r.json());
|
|
656
|
+
|
|
657
|
+
// Filter + operators + search
|
|
658
|
+
const res = await fetch("http://localhost:3070/users?age_gte=18&name_like=ana&_q=dev");
|
|
659
|
+
|
|
660
|
+
// Sort + paginate + project fields
|
|
661
|
+
const res = await fetch("http://localhost:3070/users?_sort=name&_page=1&_limit=10&_fields=id,name");
|
|
662
|
+
|
|
663
|
+
// Embed related object (parent)
|
|
664
|
+
const post = await fetch("http://localhost:3070/posts/1?_expand=user").then((r) => r.json());
|
|
665
|
+
// → { id: 1, title: "...", userId: 1, user: { id: 1, name: "Ana" } }
|
|
666
|
+
|
|
667
|
+
// Embed children
|
|
668
|
+
const user = await fetch("http://localhost:3070/users/1?_embed=posts").then((r) => r.json());
|
|
669
|
+
// → { id: 1, name: "Ana", posts: [{ id: 1, title: "First post", userId: 1 }] }
|
|
670
|
+
|
|
671
|
+
// Create
|
|
672
|
+
await fetch("http://localhost:3070/users", {
|
|
673
|
+
method: "POST",
|
|
674
|
+
headers: { "Content-Type": "application/json" },
|
|
675
|
+
body: JSON.stringify({ name: "Carlos", email: "carlos@test.com" }),
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Partial update
|
|
679
|
+
await fetch("http://localhost:3070/users/1", {
|
|
680
|
+
method: "PATCH",
|
|
681
|
+
headers: { "Content-Type": "application/json" },
|
|
682
|
+
body: JSON.stringify({ name: "Ana Updated" }),
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// Delete
|
|
686
|
+
await fetch("http://localhost:3070/users/1", { method: "DELETE" });
|
|
687
|
+
|
|
688
|
+
// Custom route with handler
|
|
689
|
+
const session = await fetch("http://localhost:3070/login", {
|
|
690
|
+
method: "POST",
|
|
691
|
+
headers: { "Content-Type": "application/json" },
|
|
692
|
+
body: JSON.stringify({ email: "ana@test.com", password: "secret" }),
|
|
693
|
+
}).then((r) => r.json());
|
|
694
|
+
// → { token: "tok-ana@test.com" }
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
## Use in package.json scripts
|
|
698
|
+
|
|
699
|
+
```json
|
|
700
|
+
{
|
|
701
|
+
"scripts": {
|
|
702
|
+
"mock": "yrest serve db.yml",
|
|
703
|
+
"mock:watch": "yrest serve db.yml --watch",
|
|
704
|
+
"mock:readonly": "yrest serve db.yml --readonly --delay 200"
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
---
|
|
710
|
+
|
|
711
|
+
## Contributing
|
|
712
|
+
|
|
713
|
+
### Prerequisites
|
|
714
|
+
|
|
715
|
+
- Node.js >= 20
|
|
716
|
+
- [Task](https://taskfile.dev) — task runner (`brew install go-task` / `scoop install task` / [other methods](https://taskfile.dev/installation/))
|
|
717
|
+
|
|
718
|
+
### Task commands
|
|
719
|
+
|
|
720
|
+
Run `task --list` to see all available commands.
|
|
721
|
+
|
|
722
|
+
#### Development
|
|
723
|
+
|
|
724
|
+
| Command | What it does |
|
|
725
|
+
| ----------------------- | ---------------------------------------------------------------------- |
|
|
726
|
+
| `task test` | Runs the full test suite once |
|
|
727
|
+
| `task test:watch` | Runs tests in watch mode — reruns on every file change |
|
|
728
|
+
| `task build` | Compiles TypeScript to `dist/` via tsup |
|
|
729
|
+
| `task dev` | Builds once, then starts watch-build + server from `dist/` in parallel |
|
|
730
|
+
| `task serve:dist` | Builds and starts the server from the local `dist/` |
|
|
731
|
+
| `task serve:dist:watch` | Builds and starts the server with `--watch` (reloads db.yml on change) |
|
|
732
|
+
| `task serve:npx` | Starts the published npm version (useful to compare against local) |
|
|
733
|
+
| `task serve:npx:watch` | Starts the published npm version with `--watch` |
|
|
734
|
+
| `task preflight` | Full pre-push check: format, lint, typecheck and tests in order |
|
|
735
|
+
|
|
736
|
+
#### Release
|
|
737
|
+
|
|
738
|
+
| Command | What it does |
|
|
739
|
+
| -------------------- | -------------------------------------------------------------- |
|
|
740
|
+
| `task release:patch` | Bumps `x.x.N`, creates a git commit and tag |
|
|
741
|
+
| `task release:minor` | Bumps `x.N.0`, creates a git commit and tag |
|
|
742
|
+
| `task release:major` | Bumps `N.0.0`, creates a git commit and tag |
|
|
743
|
+
| `task publish` | Runs tests + build and publishes to npm (requires `npm login`) |
|
|
744
|
+
|
|
745
|
+
### Development workflow
|
|
746
|
+
|
|
747
|
+
**Day-to-day work:**
|
|
748
|
+
|
|
749
|
+
```bash
|
|
750
|
+
task test:watch # keep this running in one terminal
|
|
751
|
+
task dev # keep this running in another terminal
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
`test:watch` reruns the suite on every save so you catch regressions immediately. `dev` rebuilds and serves so you can call the endpoints manually while you work.
|
|
755
|
+
|
|
756
|
+
**Before pushing:**
|
|
757
|
+
|
|
758
|
+
```bash
|
|
759
|
+
task preflight # format + lint + typecheck + tests in one command
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
**Good practices:**
|
|
763
|
+
|
|
764
|
+
- Write tests for every new feature or bug fix before opening a PR.
|
|
765
|
+
- Keep `db.yml` in a valid state — it is used as the default file when running local servers.
|
|
766
|
+
- `dist/` is gitignored and generated at build time; never commit it manually.
|
|
767
|
+
- Version bumps are done with `task release:*`, not by editing `package.json` directly — the Task command also creates the git tag that triggers the publish pipeline.
|
|
768
|
+
|
|
769
|
+
### Release workflow
|
|
770
|
+
|
|
771
|
+
Releases are fully automated via GitHub Actions once a version tag is pushed:
|
|
772
|
+
|
|
773
|
+
```bash
|
|
774
|
+
# 1. Make sure everything is clean
|
|
775
|
+
task preflight
|
|
776
|
+
|
|
777
|
+
# 2. Bump the version (choose one)
|
|
778
|
+
task release:patch # bug fixes
|
|
779
|
+
task release:minor # new features, backwards compatible
|
|
780
|
+
task release:major # breaking changes
|
|
781
|
+
|
|
782
|
+
# 3. Push the commit and the tag
|
|
783
|
+
git push && git push --tags
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
Step 3 triggers two GitHub Actions pipelines automatically:
|
|
787
|
+
|
|
788
|
+
- **CI** — runs on every push to `main` and on every PR. Executes typecheck + tests on Node 20 and Node 22.
|
|
789
|
+
- **Publish** — runs only when a `v*` tag is pushed. Runs tests + build and publishes to npm using Trusted Publishing (OIDC — no tokens stored as secrets).
|
|
790
|
+
|
|
791
|
+
### CI/CD pipelines
|
|
792
|
+
|
|
793
|
+
```
|
|
794
|
+
push to main / PR open
|
|
795
|
+
│
|
|
796
|
+
▼
|
|
797
|
+
[CI workflow]
|
|
798
|
+
├── Node 20: lint + format check + typecheck + tests
|
|
799
|
+
└── Node 22: lint + format check + typecheck + tests
|
|
800
|
+
|
|
801
|
+
push tag v*
|
|
802
|
+
│
|
|
803
|
+
▼
|
|
804
|
+
[Publish workflow]
|
|
805
|
+
├── tests + build (via prepublishOnly)
|
|
806
|
+
└── npm publish --provenance (via Trusted Publishing / OIDC, Node 24)
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
## Changelog
|
|
812
|
+
|
|
813
|
+
See [CHANGELOG.md](CHANGELOG.md) for the full version history.
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
## License
|
|
818
|
+
|
|
819
|
+
MIT
|