@koltakov/ffa-core 0.6.0 → 0.16.0

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 CHANGED
@@ -1,19 +1,24 @@
1
1
  # ffa-core
2
2
 
3
- **F**rontend **F**irst **A**PI — instant mock REST API for frontend development.
3
+ **Frontend First API** — instant mock REST API for frontend development.
4
4
 
5
- Stop waiting for the backend. Describe your data, run one command, get a working API.
5
+ Stop waiting for the backend. Describe your data in one config file, run one command, get a fully working REST API.
6
+
7
+ ```bash
8
+ npx @koltakov/ffa-core init
9
+ npx @koltakov/ffa-core dev
10
+ ```
6
11
 
7
12
  ---
8
13
 
9
14
  ## Why ffa?
10
15
 
11
- When building a frontend you need a real API to work against — but the backend isn't ready yet. `ffa` generates a fully functional REST API from a simple config file. No JSON files to maintain, no manual routes, no extra services.
12
-
13
16
  - **Zero boilerplate** — one config file, one command
14
- - **Auto CRUD** — all 6 routes per entity out of the box
17
+ - **Auto CRUD** — 6 routes per entity, out of the box
15
18
  - **Smart fake data** — field names drive generation (`email`, `price`, `avatar`, ...)
16
- - **Zod validation** — request bodies validated against your schema
19
+ - **Typed DSL** — `string().email().required()` with full TypeScript autocomplete
20
+ - **Zod validation** — POST/PUT/PATCH bodies validated against your schema
21
+ - **Relations** — `belongsTo` / `hasMany` with inline join via `?include=`
17
22
  - **Swagger UI** — auto-generated docs at `/docs`
18
23
  - **Delay simulation** — test loading states with artificial latency
19
24
  - **TypeScript or JSON** — pick your config format
@@ -23,87 +28,94 @@ When building a frontend you need a real API to work against — but the backend
23
28
  ## Quick Start
24
29
 
25
30
  ```bash
26
- # 1. Install
27
31
  npm install @koltakov/ffa-core
28
32
 
29
- # 2. Scaffold a config
30
- npx ffa init
31
-
32
- # 3. Start
33
- npx ffa dev
33
+ npx ffa init # scaffold ffa.config.ts
34
+ npx ffa dev # start server
34
35
  ```
35
36
 
36
- Or skip `init` and write `ffa.config.ts` manually (see below).
37
-
38
37
  ---
39
38
 
40
- ## Config: TypeScript
39
+ ## Config
41
40
 
42
41
  ```ts
43
- import { defineConfig, entity, string, number, boolean, enumField, belongsTo } from '@koltakov/ffa-core'
42
+ // ffa.config.ts
43
+ import {
44
+ defineConfig, entity,
45
+ string, number, boolean, enumField,
46
+ belongsTo, hasMany, object, array,
47
+ } from '@koltakov/ffa-core'
44
48
 
45
49
  export default defineConfig({
46
50
  server: {
47
51
  port: 3333,
48
- delay: [200, 600], // artificial latency (ms or [min, max])
52
+ delay: [200, 600], // artificial latency in ms (number or [min, max])
49
53
  persist: true, // save data to ffa-data.json between restarts
54
+ errorRate: 0.05, // 5% of requests return 500 (for error state testing)
50
55
  },
51
56
 
52
57
  entities: {
53
58
  User: entity({
54
- name: string().required(),
55
- email: string().required(),
56
- role: enumField(['admin', 'editor', 'viewer']).required(),
57
- avatar: string().optional(),
58
- }, { count: 20 }),
59
+ name: string().fullName().required(),
60
+ email: string().email().required(),
61
+ role: enumField(['admin', 'editor', 'viewer']).required(),
62
+ avatar: string().avatar().optional(),
63
+ address: object({
64
+ city: string().city(),
65
+ country: string().country(),
66
+ zip: string().zip(),
67
+ }),
68
+ phones: array(string().phone()),
69
+ }, {
70
+ count: 20,
71
+ // Guaranteed records — appear first, rest are generated up to count
72
+ seed: [
73
+ { name: 'Admin User', email: 'admin@example.com', role: 'admin' },
74
+ ],
75
+ }),
59
76
 
60
77
  Post: entity({
61
- title: string().required(),
62
- body: string().required(),
63
- status: enumField(['draft', 'published', 'archived']).required(),
64
- price: number().min(0).max(9999).required(),
78
+ title: string().sentence().required(),
79
+ body: string().paragraph().required(),
80
+ status: enumField(['draft', 'published', 'archived']).required(),
81
+ price: number().price().required(),
82
+ rating: number().rating().optional(),
65
83
  authorId: belongsTo('User'),
84
+ tagIds: hasMany('Tag'),
85
+ }, {
86
+ count: 50,
87
+ meta: {
88
+ currency: 'USD', // static value
89
+ total: (items) => items.length, // computed from filtered items
90
+ avgPrice: (items) => +(
91
+ items.reduce((s, i) => s + (i.price as number), 0) / items.length
92
+ ).toFixed(2),
93
+ },
66
94
  }),
95
+
96
+ Tag: entity({
97
+ name: string().word().required(),
98
+ color: string().hexColor().optional(),
99
+ }, { count: 15 }),
67
100
  },
68
101
  })
69
102
  ```
70
103
 
71
- ## Config: JSON (no TypeScript needed)
72
-
73
- ```json
74
- {
75
- "server": { "port": 3333, "delay": 300 },
76
- "entities": {
77
- "Product": {
78
- "count": 20,
79
- "fields": {
80
- "title": "string!",
81
- "price": "number",
82
- "status": ["draft", "published", "archived"],
83
- "inStock": "boolean",
84
- "createdAt": "datetime"
85
- }
86
- }
87
- }
88
- }
89
- ```
90
-
91
- Save as `ffa.config.json`. Type suffix `!` means required (`"string!"` → required string). An array is shorthand for `enumField`.
92
-
93
104
  ---
94
105
 
95
106
  ## CLI
96
107
 
97
- ```bash
98
- ffa dev # start server
99
- ffa dev --port 4000 # override port
100
- ffa dev --watch # restart on ffa.config.ts changes
101
- ffa dev --open # open Swagger UI in browser on start
102
- ffa dev -p 4000 -w -o # all flags combined
103
-
104
- ffa init # create ffa.config.ts in current directory
105
- ffa reset # delete ffa-data.json (clears persisted data)
106
- ```
108
+ | Command | Description |
109
+ |---------|-------------|
110
+ | `ffa dev` | Start the dev server |
111
+ | `ffa dev -p 4000` | Override port |
112
+ | `ffa dev -w` | Watch mode restart on config changes |
113
+ | `ffa dev -o` | Open Swagger UI in browser on start |
114
+ | `ffa init` | Scaffold `ffa.config.ts` in current directory |
115
+ | `ffa inspect` | Show config structure without starting the server |
116
+ | `ffa snapshot` | Export current in-memory DB to JSON (server must be running) |
117
+ | `ffa snapshot -o dump.json` | Custom output path |
118
+ | `ffa reset` | Delete `ffa-data.json` (clears persisted data) |
107
119
 
108
120
  ---
109
121
 
@@ -111,167 +123,399 @@ ffa reset # delete ffa-data.json (clears persisted data)
111
123
 
112
124
  For every entity `Foo` ffa creates six routes:
113
125
 
114
- | Method | Path | Description | Status |
115
- |-----------|--------------|--------------------------------------|--------------|
116
- | `GET` | `/foos` | List all records | 200 |
117
- | `GET` | `/foos/:id` | Get single record | 200, 404 |
118
- | `POST` | `/foos` | Create a record | 201, 422 |
119
- | `PUT` | `/foos/:id` | Full replace (all fields required) | 200, 404, 422|
120
- | `PATCH` | `/foos/:id` | Partial update (all fields optional) | 200, 404, 422|
121
- | `DELETE` | `/foos/:id` | Delete a record | 200, 404 |
126
+ | Method | Path | Description | Status |
127
+ |--------|------|-------------|--------|
128
+ | `GET` | `/foos` | List all records | 200 |
129
+ | `GET` | `/foos/:id` | Get single record | 200, 404 |
130
+ | `POST` | `/foos` | Create a record | 201, 422 |
131
+ | `PUT` | `/foos/:id` | Full replace | 200, 404, 422 |
132
+ | `PATCH` | `/foos/:id` | Partial update | 200, 404, 422 |
133
+ | `DELETE` | `/foos/:id` | Delete a record | 200, 404 |
122
134
 
123
135
  > Names are auto-pluralized: `Product` → `/products`, `Category` → `/categories`.
124
136
 
137
+ ### System endpoints
138
+
139
+ | Method | Path | Description |
140
+ |--------|------|-------------|
141
+ | `POST` | `/__reset` | Regenerate all data |
142
+ | `GET` | `/__snapshot` | Dump current DB to JSON |
143
+ | `GET` | `/docs` | Swagger UI |
144
+ | `GET` | `/openapi.json` | OpenAPI 3.0 spec |
145
+
125
146
  ---
126
147
 
127
- ## Query Params on GET /list
148
+ ## Query Params (GET /list)
149
+
150
+ ### Pagination, sorting, search
151
+
152
+ ```
153
+ ?page=2&limit=10
154
+ ?sort=price&order=desc
155
+ ?search=apple full-text search across all string fields
156
+ ?status=published exact match filter
157
+ ```
158
+
159
+ ### Filter operators
128
160
 
129
- | Param | Example | Description |
130
- |---------------|------------------------------|-------------------------------------|
131
- | `page` | `?page=2` | Page number (default: 1) |
132
- | `limit` | `?limit=10` | Records per page |
133
- | `sort` | `?sort=price` | Sort by field |
134
- | `order` | `?order=desc` | `asc` (default) or `desc` |
135
- | `search` | `?search=apple` | Full-text search across string fields|
136
- | `{field}` | `?status=published` | Exact field filter |
161
+ ```
162
+ ?price_gte=100 price >= 100
163
+ ?price_lte=500 price <= 500
164
+ ?price_gt=0 price > 0
165
+ ?price_lt=1000 price < 1000
166
+ ?price_ne=0 price != 0
167
+ ?status_in=draft,published status in ['draft', 'published']
168
+ ?title_contains=hello title contains 'hello' (case-insensitive)
169
+ ```
137
170
 
138
171
  All params are combinable:
139
172
  ```
140
- GET /posts?search=react&status=published&sort=price&order=asc&page=1&limit=20
173
+ GET /posts?search=react&status_in=draft,published&price_gte=10&sort=price&order=asc&page=1&limit=20
141
174
  ```
142
175
 
143
- **Response envelope:**
176
+ ### Response envelope
177
+
144
178
  ```json
145
179
  {
146
- "data": [ { "id": "...", "title": "...", ... } ],
147
- "meta": { "total": 47, "page": 1, "limit": 20, "pages": 3 }
180
+ "data": [{ "id": "...", "title": "...", "price": 49.99 }],
181
+ "meta": {
182
+ "pagination": { "total": 47, "page": 1, "limit": 10, "pages": 5 },
183
+ "currency": "USD",
184
+ "avgPrice": 124.50
185
+ }
148
186
  }
149
187
  ```
150
188
 
151
- Also sets `X-Total-Count: 47` header.
189
+ Header: `X-Total-Count: 47`
152
190
 
153
191
  ---
154
192
 
155
- ## Relations
193
+ ## Field Types
194
+
195
+ ### Primitives
156
196
 
157
197
  ```ts
158
- import { belongsTo, hasMany } from '@koltakov/ffa-core'
198
+ string() // chainable hints see below
199
+ number() // chainable hints — see below
200
+ boolean()
201
+ uuid()
202
+ datetime()
203
+ ```
159
204
 
160
- entities: {
161
- User: entity({ name: string().required() }),
205
+ ### Enum
162
206
 
163
- Post: entity({
164
- title: string().required(),
165
- authorId: belongsTo('User'), // stores a random User id
166
- tagIds: hasMany('Tag'), // stores 1–3 random Tag ids
167
- }),
168
- }
207
+ ```ts
208
+ enumField(['draft', 'published', 'archived'])
169
209
  ```
170
210
 
171
- **Inline join** on GET by id:
211
+ ### Relations
212
+
213
+ ```ts
214
+ belongsTo('User') // stores a random User id
215
+ hasMany('Tag') // stores an array of 1–3 Tag ids
216
+ ```
217
+
218
+ **Inline join** on `GET /:id`:
172
219
  ```
173
220
  GET /posts/abc?include=authorId,tagIds
174
221
  ```
175
- Returns the related objects inline instead of just ids.
222
+
223
+ ### Nested structures (v0.12.0)
224
+
225
+ ```ts
226
+ // Nested object — generates each field recursively
227
+ address: object({
228
+ city: string().city(),
229
+ country: string().country(),
230
+ zip: string().zip(),
231
+ })
232
+
233
+ // Array of typed items
234
+ tags: array(string().word())
235
+ phones: array(string().phone(), [1, 4]) // [min, max] items
236
+ scores: array(number().rating())
237
+ ```
176
238
 
177
239
  ---
178
240
 
179
- ## Field Types
241
+ ## String Fake Hints
180
242
 
181
- | Factory | Faker output | Notes |
182
- |-----------------------|----------------------------------|--------------------------------|
183
- | `string()` | Smart by field name or word | See smart faker below |
184
- | `number()` | Integer 0–1000 | Respects `.min()` / `.max()` |
185
- | `boolean()` | `true` / `false` | |
186
- | `uuid()` | UUID v4 | |
187
- | `datetime()` | ISO 8601 string | |
188
- | `enumField([...])` | Random value from array | Also validates on write |
189
- | `belongsTo('Entity')` | Random id from that entity | |
190
- | `hasMany('Entity')` | Array of 1–3 ids from that entity| |
191
-
192
- ### Smart Faker by Field Name
193
-
194
- Field names drive faker output automatically:
195
-
196
- | Field name pattern | Generated value |
197
- |-----------------------------|-------------------------------|
198
- | `email`, `mail` | `faker.internet.email()` |
199
- | `name`, `firstName` | `faker.person.firstName()` |
200
- | `lastName`, `surname` | `faker.person.lastName()` |
201
- | `phone`, `tel`, `mobile` | `faker.phone.number()` |
202
- | `city`, `country`, `address`| Location values |
203
- | `url`, `website`, `link` | `faker.internet.url()` |
204
- | `avatar`, `photo`, `image` | `faker.image.avatar()` |
205
- | `company` | `faker.company.name()` |
206
- | `title`, `heading` | Short sentence |
207
- | `description`, `bio`, `text`| Paragraph |
208
- | `price`, `cost`, `amount` | `faker.commerce.price()` |
209
- | `color` | `faker.color.human()` |
243
+ Chain a method on `string()` to control what gets generated:
244
+
245
+ ```ts
246
+ // Internet
247
+ string().email() // "user@example.com"
248
+ string().url() // "https://example.com"
249
+ string().domain() // "example.com"
250
+ string().ip() // "192.168.1.1"
251
+ string().username() // "john_doe"
252
+
253
+ // Media
254
+ string().image() // "https://loremflickr.com/640/480"
255
+ string().avatar() // avatar URL
256
+
257
+ // Person
258
+ string().firstName() // "Alice"
259
+ string().lastName() // "Johnson"
260
+ string().fullName() // "Alice Johnson"
261
+ string().phone() // "+1-555-234-5678"
262
+
263
+ // Location
264
+ string().city() // "Berlin"
265
+ string().country() // "Germany"
266
+ string().address() // "12 Oak Street"
267
+ string().zip() // "10115"
268
+ string().locale() // "DE"
269
+
270
+ // Business
271
+ string().company() // "Acme Corp"
272
+ string().jobTitle() // "Senior Engineer"
273
+ string().department() // "Electronics"
274
+ string().currency() // "EUR"
275
+
276
+ // Text
277
+ string().word() // "matrix"
278
+ string().slug() // "hello-world"
279
+ string().sentence() // "The quick brown fox."
280
+ string().paragraph() // "Lorem ipsum dolor..."
281
+ string().bio() // "Lorem ipsum dolor..."
282
+
283
+ // Visual
284
+ string().color() // "azure"
285
+ string().hexColor() // "#A3F5C2"
286
+
287
+ // Id
288
+ string().uuid() // UUID v4
289
+ ```
290
+
291
+ ## Number Fake Hints
292
+
293
+ ```ts
294
+ number().price() // 19.99
295
+ number().age() // 34
296
+ number().rating() // 4
297
+ number().percent() // 72
298
+ number().lat() // 51.5074
299
+ number().lng() // -0.1278
300
+ number().year() // 2021
301
+ ```
302
+
303
+ ## Smart Field-Name Detection
304
+
305
+ When no hint is set, ffa infers the right value from the field name automatically:
306
+
307
+ | Field name pattern | Generated value |
308
+ |--------------------|----------------|
309
+ | `email`, `mail` | Email address |
310
+ | `name`, `firstName` | First name |
311
+ | `lastName`, `surname` | Last name |
312
+ | `phone`, `tel`, `mobile` | Phone number |
313
+ | `city`, `country`, `address` | Location values |
314
+ | `url`, `website`, `link` | URL |
315
+ | `avatar`, `photo` | Avatar URL |
316
+ | `image` | Image URL |
317
+ | `company` | Company name |
318
+ | `title`, `heading` | Short sentence |
319
+ | `description`, `bio`, `text` | Paragraph |
320
+ | `price`, `cost`, `amount` | Price |
321
+ | `color` | Color name |
210
322
 
211
323
  ---
212
324
 
213
325
  ## Field Rules
214
326
 
215
- | Rule | Applies to | Effect |
216
- |---------------|--------------------|-------------------------------------------------|
217
- | `.required()` | all | Field required in POST/PUT requests |
218
- | `.optional()` | all | Field can be omitted |
219
- | `.min(n)` | `string`, `number` | Min length / min value |
220
- | `.max(n)` | `string`, `number` | Max length / max value |
221
- | `.readonly()` | all | Excluded from create/update validation |
327
+ All builders share these chainable rules:
328
+
329
+ | Method | Effect |
330
+ |--------|--------|
331
+ | `.required()` | Field required in POST/PUT |
332
+ | `.optional()` | Field can be omitted (default) |
333
+ | `.min(n)` | Min string length / min number value |
334
+ | `.max(n)` | Max string length / max number value |
335
+ | `.readonly()` | Excluded from create/update validation |
336
+ | `.default(val)` | Default value |
337
+ | `.fake(hint)` | Explicit faker hint |
222
338
 
223
339
  ---
224
340
 
225
- ## Server Config
341
+ ## Seed Data (v0.9.0)
342
+
343
+ Guarantee specific records always exist in your entity:
226
344
 
227
345
  ```ts
228
- server: {
229
- port: 3333, // default: 3000
230
- delay: 400, // fixed delay in ms
231
- delay: [200, 800], // random delay between min and max ms
232
- persist: true, // save to ./ffa-data.json
233
- persist: './data.json', // custom file path
234
- }
346
+ User: entity({
347
+ name: string().fullName().required(),
348
+ role: enumField(['admin', 'editor', 'viewer']).required(),
349
+ }, {
350
+ count: 20,
351
+ seed: [
352
+ { id: 'admin-1', name: 'Admin User', role: 'admin' },
353
+ { name: 'Guest User', role: 'viewer' }, // id auto-generated if omitted
354
+ ],
355
+ })
235
356
  ```
236
357
 
237
- If `delay` is set, system routes (`/__reset`, `/docs`) are not delayed.
358
+ Seed records appear first. The remaining `count - seed.length` records are generated.
238
359
 
239
- If the configured port is busy, ffa automatically tries the next one.
360
+ ---
361
+
362
+ ## Custom Meta (v0.8.0)
363
+
364
+ Attach extra fields to the list response `meta`. Values can be static or computed from the current filtered dataset (before pagination):
365
+
366
+ ```ts
367
+ Product: entity({ price: number().price(), status: enumField(['active', 'sale']) }, {
368
+ count: 50,
369
+ meta: {
370
+ currency: 'USD', // static
371
+ apiVersion: 2, // static
372
+ total: (items) => items.length, // computed
373
+ avgPrice: (items) => +(items.reduce((s, i) => s + (i.price as number), 0) / items.length).toFixed(2),
374
+ byStatus: (items) => Object.fromEntries(
375
+ ['active', 'sale'].map(s => [s, items.filter(i => i.status === s).length])
376
+ ),
377
+ },
378
+ })
379
+ ```
380
+
381
+ Meta functions receive items **after** search/filter but **before** pagination — so aggregates always reflect the current query.
382
+
383
+ ---
384
+
385
+ ## Seed Data Export (v0.15.0)
386
+
387
+ Export the current in-memory DB while the server is running — useful for generating fixture files:
388
+
389
+ ```bash
390
+ ffa snapshot # → ffa-snapshot.json
391
+ ffa snapshot -o fixtures.json
392
+ ffa snapshot -p 4000 # specify port if not in config
393
+ ```
394
+
395
+ Or call directly:
396
+ ```
397
+ GET /__snapshot → { "User": [...], "Post": [...] }
398
+ ```
240
399
 
241
400
  ---
242
401
 
243
- ## System Endpoints
402
+ ## Inspect Config (v0.16.0)
403
+
404
+ Print your config structure without starting the server:
405
+
406
+ ```bash
407
+ ffa inspect
408
+ ```
409
+
410
+ ```
411
+ FFA inspect port 3333
412
+
413
+ User 20 records +1 seed
414
+ * name string [fullName]
415
+ * email string [email]
416
+ * role enum (admin | editor | viewer)
417
+ avatar string [avatar]
418
+ address object { city, country, zip }
419
+ phones array of string
420
+
421
+ Post 50 records
422
+ * title string [sentence]
423
+ * body string [paragraph]
424
+ * status enum (draft | published | archived)
425
+ * price number [price]
426
+ authorId belongsTo → User
427
+ tagIds hasMany → Tag
428
+ meta: currency, total, avgPrice
429
+
430
+ delay 200–600ms · persist ffa-data.json · errorRate 5%
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Config Validation (v0.11.0)
436
+
437
+ On startup, ffa checks your config and prints warnings:
438
+
439
+ ```
440
+ ⚠ Post.authorId (belongsTo) references 'Author' which is not defined
441
+ ⚠ Tag.type is enum but has no values
442
+ ⚠ User: seed has 5 records but count is 3 — seed will be truncated
443
+ ```
444
+
445
+ ---
244
446
 
245
- | Method | Path | Description |
246
- |--------|----------------|------------------------------------|
247
- | `POST` | `/__reset` | Regenerate all seed data in memory |
248
- | `GET` | `/docs` | Swagger UI |
249
- | `GET` | `/openapi.json`| Raw OpenAPI 3.0 spec |
447
+ ## Error Simulation (v0.14.0)
448
+
449
+ Test your frontend error handling without mocking:
250
450
 
251
451
  ```ts
252
- // Reset data from frontend code (e.g. in test setup)
253
- await fetch('http://localhost:3333/__reset', { method: 'POST' })
452
+ server: {
453
+ errorRate: 0.1 // 10% of requests return 500
454
+ }
254
455
  ```
255
456
 
457
+ System routes (`/__reset`, `/__snapshot`, `/docs`) are never affected.
458
+
256
459
  ---
257
460
 
258
- ## Terminal Output
461
+ ## Persistence
259
462
 
260
- Every request is logged with method, URL, status code and response time:
463
+ ```ts
464
+ server: {
465
+ persist: true, // save to ./ffa-data.json
466
+ persist: './data/db.json', // custom path
467
+ }
468
+ ```
261
469
 
470
+ Data is saved on every write (POST/PUT/PATCH/DELETE) and restored on restart.
471
+
472
+ ---
473
+
474
+ ## JSON Config (no TypeScript)
475
+
476
+ ```json
477
+ {
478
+ "server": { "port": 3333, "delay": 300 },
479
+ "entities": {
480
+ "Product": {
481
+ "count": 20,
482
+ "fields": {
483
+ "title": "string!",
484
+ "price": "number",
485
+ "status": ["draft", "published", "archived"],
486
+ "inStock": "boolean",
487
+ "createdAt": "datetime"
488
+ }
489
+ }
490
+ }
491
+ }
262
492
  ```
263
- FFA dev server v0.6.0
264
493
 
265
- Product 20 records http://localhost:3333/products
266
- → User 10 records http://localhost:3333/users
494
+ Save as `ffa.config.json`. `"string!"` = required, `"string"` = optional. Arrays are shorthand for `enumField`.
495
+
496
+ ---
497
+
498
+ ## Terminal Output
499
+
500
+ ```
501
+ FFA dev server v0.16.0
502
+
503
+ Entity Count Route
504
+ ──────────────────────────────────────────────────────────
505
+ User 20 GET POST http://localhost:3333/users
506
+ Post 50 GET POST http://localhost:3333/posts
507
+ Tag 15 GET POST http://localhost:3333/tags
508
+
509
+ delay 200–600ms
510
+ errorRate 5% chance of 500
267
511
 
268
- delay: 200–600ms
269
- Swagger UI http://localhost:3333/docs
512
+ docs http://localhost:3333/docs
513
+ reset POST http://localhost:3333/__reset
270
514
 
271
- GET /products 200 312ms
272
- POST /products 201 287ms
273
- GET /products/abc-123 404 201ms
274
- PATCH /products/def-456 200 344ms
515
+ GET /users 200 14ms
516
+ POST /posts 201 312ms
517
+ GET /posts/abc-123 404 8ms
518
+ PATCH /posts/def-456 200 267ms
275
519
  ```
276
520
 
277
521
  ---