@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 ADDED
@@ -0,0 +1,819 @@
1
+ # yRest
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@yrest/cli)](https://www.npmjs.com/package/@yrest/cli)
4
+ [![npm downloads](https://img.shields.io/npm/dw/@yrest/cli)](https://www.npmjs.com/package/@yrest/cli)
5
+ [![license](https://img.shields.io/npm/l/@yrest/cli)](LICENSE)
6
+ [![CI](https://github.com/aggiovato/yRest/actions/workflows/ci.yml/badge.svg)](https://github.com/aggiovato/yRest/actions)
7
+ [![Node](https://img.shields.io/node/v/@yrest/cli)](https://www.npmjs.com/package/@yrest/cli)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue)](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