@rip-lang/server 1.3.115 → 1.3.117

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.
Files changed (44) hide show
  1. package/README.md +435 -622
  2. package/api.rip +4 -4
  3. package/control/cli.rip +221 -1
  4. package/control/control.rip +9 -0
  5. package/control/lifecycle.rip +6 -1
  6. package/control/watchers.rip +10 -0
  7. package/control/workers.rip +9 -5
  8. package/default.rip +3 -1
  9. package/docs/READ_VALIDATORS.md +656 -0
  10. package/docs/edge/CONFIG_LIFECYCLE.md +70 -32
  11. package/docs/edge/CONTRACTS.md +60 -69
  12. package/docs/edge/EDGEFILE_CONTRACT.md +258 -29
  13. package/docs/edge/M0B_REVIEW_NOTES.md +5 -5
  14. package/edge/config.rip +584 -52
  15. package/edge/forwarding.rip +6 -2
  16. package/edge/metrics.rip +19 -1
  17. package/edge/registry.rip +29 -3
  18. package/edge/router.rip +138 -0
  19. package/edge/runtime.rip +98 -0
  20. package/edge/static.rip +69 -0
  21. package/edge/tls.rip +23 -0
  22. package/edge/upstream.rip +272 -0
  23. package/edge/verify.rip +73 -0
  24. package/middleware.rip +3 -3
  25. package/package.json +2 -2
  26. package/server.rip +775 -393
  27. package/tests/control.rip +18 -0
  28. package/tests/edgefile.rip +165 -0
  29. package/tests/metrics.rip +16 -0
  30. package/tests/proxy.rip +22 -1
  31. package/tests/registry.rip +27 -0
  32. package/tests/router.rip +101 -0
  33. package/tests/runtime_entrypoints.rip +16 -0
  34. package/tests/servers.rip +262 -0
  35. package/tests/static.rip +64 -0
  36. package/tests/streams_clienthello.rip +108 -0
  37. package/tests/streams_index.rip +53 -0
  38. package/tests/streams_pipe.rip +70 -0
  39. package/tests/streams_router.rip +39 -0
  40. package/tests/streams_runtime.rip +38 -0
  41. package/tests/streams_upstream.rip +34 -0
  42. package/tests/upstream.rip +191 -0
  43. package/tests/verify.rip +148 -0
  44. package/tests/watchers.rip +15 -0
@@ -0,0 +1,656 @@
1
+ # Validation Reference — `read()`
2
+
3
+ > Extracted from the main [@rip-lang/server README](../README.md).
4
+
5
+ The `read()` function is a validation and parsing powerhouse that eliminates
6
+ 90% of API boilerplate.
7
+
8
+ ### Basic Patterns
9
+
10
+ ```coffee
11
+ # Required field (throws if missing)
12
+ email = read 'email', 'email!'
13
+
14
+ # Optional field (returns null if missing)
15
+ phone = read 'phone', 'phone'
16
+
17
+ # With default value
18
+ role = read 'role', ['admin', 'user'], 'user'
19
+
20
+ # Get entire payload
21
+ data = read()
22
+ ```
23
+
24
+ ### Range Validation
25
+
26
+ The `[min, max]` syntax works for both numbers and string lengths:
27
+
28
+ ```coffee
29
+ # Numbers: value range
30
+ age = read 'age', 'int', [18, 120] # Between 18 and 120
31
+ priority = read 'priority', 'int', [1, 10] # 1-10 range
32
+
33
+ # Strings: length range
34
+ username = read 'username', 'string', [3, 20] # 3-20 characters
35
+ bio = read 'bio', 'string', [0, 500] # Up to 500 chars
36
+
37
+ # Named parameters
38
+ views = read 'views', 'int', min: 0 # Non-negative integer
39
+ discount = read 'discount', 'float', max: 100 # Up to 100
40
+ ```
41
+
42
+ ### Enumeration Validation
43
+
44
+ ```coffee
45
+ # Must be one of these values
46
+ role = read 'role', ['admin', 'user', 'guest']
47
+ status = read 'status', ['pending', 'active', 'closed']
48
+ ```
49
+
50
+ ### Regex Validation
51
+
52
+ ```coffee
53
+ # Custom pattern matching
54
+ code = read 'code', /^[A-Z]{3,6}$/
55
+ ```
56
+
57
+ ## Built-in Validators
58
+
59
+ `@rip-lang/server` includes 37 validators for every common API need:
60
+
61
+ ### Numbers & Money
62
+ ```coffee
63
+ id = read 'user_id', 'id!' # Positive integer (1+)
64
+ count = read 'count', 'whole' # Non-negative integer (0+)
65
+ price = read 'price', 'float' # Decimal number
66
+ cost = read 'cost', 'money' # Banker's rounding to cents
67
+ ```
68
+
69
+ ### Text Processing
70
+ ```coffee
71
+ title = read 'title', 'string' # Collapses whitespace
72
+ bio = read 'bio', 'text' # Light cleanup
73
+ name = read 'name', 'name' # Trims and normalizes
74
+ ```
75
+
76
+ ### Contact Information
77
+ ```coffee
78
+ email = read 'email', 'email' # Valid email format
79
+ phone = read 'phone', 'phone' # US phone → (555) 123-4567
80
+ address = read 'address', 'address' # Trimmed address
81
+ ```
82
+
83
+ ### Geographic Data
84
+ ```coffee
85
+ state = read 'state', 'state' # Two-letter → uppercase
86
+ zip = read 'zip', 'zip' # 5-digit zip
87
+ zipplus4 = read 'zip', 'zipplus4' # 12345-6789 format
88
+ ```
89
+
90
+ ### Identity & Security
91
+ ```coffee
92
+ ssn = read 'ssn', 'ssn' # SSN → digits only
93
+ sex = read 'gender', 'sex' # m/f/o
94
+ username = read 'username', 'username' # 3-20 chars, lowercase
95
+ ```
96
+
97
+ ### Web & Technical
98
+ ```coffee
99
+ url = read 'website', 'url' # Valid URL
100
+ ip = read 'ip_address', 'ip' # IPv4 address
101
+ mac = read 'mac', 'mac' # MAC address
102
+ color = read 'color', 'color' # Hex color → #abc123
103
+ uuid = read 'user_id', 'uuid' # UUID format
104
+ semver = read 'version', 'semver' # Semantic version
105
+ ```
106
+
107
+ ### Time & Date
108
+ ```coffee
109
+ date = read 'date', 'date' # YYYY-MM-DD
110
+ time = read 'time', 'time' # HH:MM or HH:MM:SS (24-hour)
111
+ time12 = read 'time', 'time12' # 12-hour with am/pm
112
+ ```
113
+
114
+ ### Boolean & Collections
115
+ ```coffee
116
+ active = read 'active', 'truthy' # true/t/1/yes/y/on → true
117
+ inactive = read 'off', 'falsy' # false/f/0/no/n/off → true
118
+ flag = read 'flag', 'bool' # Either → boolean
119
+ tags = read 'tags', 'array' # Must be array
120
+ config = read 'config', 'hash' # Must be object
121
+ settings = read 'data', 'json' # Parse JSON string
122
+ ids = read 'ids', 'ids' # "1,2,3" → [1, 2, 3]
123
+ slug = read 'slug', 'slug' # URL-safe slug
124
+ ```
125
+
126
+ ### Custom Validators
127
+
128
+ ```coffee
129
+ import { registerValidator, read } from '@rip-lang/server'
130
+
131
+ registerValidator 'postalCode', (v) ->
132
+ if v =~ /^[A-Z]\d[A-Z] \d[A-Z]\d$/i
133
+ _[0].toUpperCase()
134
+ else
135
+ null
136
+
137
+ # Now use it
138
+ code = read 'postal', 'postalCode!'
139
+ ```
140
+
141
+ ## Routing
142
+
143
+ ### HTTP Methods
144
+
145
+ ```coffee
146
+ import { get, post, put, patch, del, all } from '@rip-lang/server'
147
+
148
+ get '/users' -> listUsers!
149
+ post '/users' -> createUser!
150
+ get '/users/:id' -> getUser!
151
+ put '/users/:id' -> updateUser!
152
+ patch '/users/:id' -> patchUser!
153
+ del '/users/:id' -> deleteUser!
154
+ all '/health' -> 'ok' # All methods
155
+ ```
156
+
157
+ ### Path Parameters
158
+
159
+ ```coffee
160
+ # Basic parameters
161
+ get '/users/:id' ->
162
+ id = read 'id', 'id!'
163
+ { id }
164
+
165
+ # Multiple parameters
166
+ get '/users/:userId/posts/:postId' ->
167
+ userId = read 'userId', 'id!'
168
+ postId = read 'postId', 'id!'
169
+ { userId, postId }
170
+
171
+ # Custom patterns
172
+ get '/files/:name{[a-z]+\\.txt}' ->
173
+ name = read 'name'
174
+ { file: name }
175
+
176
+ # Wildcards
177
+ get '/static/*', (env) ->
178
+ { path: env.req.path }
179
+ ```
180
+
181
+ ### Route Grouping
182
+
183
+ ```coffee
184
+ import { prefix } from '@rip-lang/server'
185
+
186
+ prefix '/api/v1' ->
187
+ get '/users' -> listUsers!
188
+ get '/posts' -> listPosts!
189
+
190
+ prefix '/api/v2' ->
191
+ get '/users' -> listUsersV2!
192
+ ```
193
+
194
+ ## Middleware
195
+
196
+ ### Built-in Middleware
197
+
198
+ Import from `@rip-lang/server/middleware`:
199
+
200
+ ```coffee
201
+ import { use } from '@rip-lang/server'
202
+ import { cors, logger, compress, sessions, secureHeaders, timeout, bodyLimit } from '@rip-lang/server/middleware'
203
+
204
+ # Logging
205
+ use logger()
206
+ use logger format: 'tiny' # Minimal output
207
+ use logger format: 'dev' # Colorized (default)
208
+ use logger skip: (c) -> c.req.path is '/health'
209
+
210
+ # CORS
211
+ use cors() # Allow all origins
212
+ use cors origin: 'https://myapp.com' # Specific origin
213
+ use cors origin: ['https://a.com', 'https://b.com']
214
+ use cors credentials: true, maxAge: 86400
215
+
216
+ # Compression (gzip/deflate)
217
+ use compress()
218
+ use compress threshold: 1024 # Min bytes to compress
219
+
220
+ # Security headers
221
+ use secureHeaders()
222
+ use secureHeaders hsts: true, contentSecurityPolicy: "default-src 'self'"
223
+
224
+ # Request limits
225
+ use timeout ms: 30000 # 30 second timeout
226
+ use bodyLimit maxSize: 1024 * 1024 # 1MB max body
227
+ ```
228
+
229
+ ### Middleware Options
230
+
231
+ | Middleware | Options |
232
+ |------------|---------|
233
+ | `logger()` | `format`, `skip`, `stream` |
234
+ | `cors()` | `origin`, `methods`, `headers`, `credentials`, `maxAge`, `exposeHeaders`, `preflight` |
235
+ | `compress()` | `threshold`, `encodings` |
236
+ | `sessions()` | `secret`, `name`, `maxAge`, `secure`, `httpOnly`, `sameSite` |
237
+ | `secureHeaders()` | `hsts`, `hstsMaxAge`, `contentSecurityPolicy`, `frameOptions`, `referrerPolicy` |
238
+ | `timeout()` | `ms`, `message`, `status` |
239
+ | `bodyLimit()` | `maxSize`, `message` |
240
+
241
+ ### Session Usage
242
+
243
+ ```coffee
244
+ import { get, use, before, session } from '@rip-lang/server'
245
+ import { sessions } from '@rip-lang/server/middleware'
246
+
247
+ # Sessions parses cookies directly from request headers
248
+ use sessions secret: process.env.SESSION_SECRET
249
+
250
+ before ->
251
+ session.views ?= 0
252
+ session.views += 1
253
+
254
+ get '/profile' ->
255
+ { userId: session.userId, views: session.views }
256
+
257
+ get '/login' ->
258
+ session.userId = 123
259
+ { loggedIn: true }
260
+
261
+ get '/logout' ->
262
+ delete session.userId
263
+ { loggedOut: true }
264
+ ```
265
+
266
+ The `session` import works anywhere via AsyncLocalStorage — no `@` needed, works in helpers and nested callbacks.
267
+
268
+ **Security note:** Without `secret`, sessions use plain base64 (dev only). With `secret`, sessions are HMAC-SHA256 signed (tamper-proof). Always set `secret` in production.
269
+
270
+ ### CORS with Preflight
271
+
272
+ ```coffee
273
+ import { use } from '@rip-lang/server'
274
+ import { cors } from '@rip-lang/server/middleware'
275
+
276
+ # Handle OPTIONS early (before routes are matched)
277
+ use cors origin: 'https://myapp.com', preflight: true
278
+ ```
279
+
280
+ ### Custom Middleware
281
+
282
+ ```coffee
283
+ # Authentication middleware
284
+ use (c, next) ->
285
+ token = @req.header 'Authorization'
286
+ unless token
287
+ return @json { error: 'Unauthorized' }, 401
288
+ @user = validateToken!(token)
289
+ await next()
290
+
291
+ # Timing middleware
292
+ use (c, next) ->
293
+ start = Date.now()
294
+ await next()
295
+ @header 'X-Response-Time', "#{Date.now() - start}ms"
296
+ ```
297
+
298
+ ### Request Lifecycle Filters
299
+
300
+ Three filters run at different stages: `raw` → `before` → handler → `after`
301
+
302
+ ```coffee
303
+ import { raw, before, after, get } from '@rip-lang/server'
304
+
305
+ # Runs first — modify raw request before body parsing
306
+ raw (req) ->
307
+ if req.headers.get('X-Raw-SQL') is 'true'
308
+ req.headers.set 'content-type', 'text/plain'
309
+
310
+ skipPaths = ['/favicon.ico', '/ping', '/health']
311
+
312
+ # Runs before handler (after body parsing)
313
+ before ->
314
+ @start = Date.now()
315
+ @silent = @req.path in skipPaths
316
+ unless @req.header 'Authorization'
317
+ return @json { error: 'Unauthorized' }, 401
318
+
319
+ # Runs after handler
320
+ after ->
321
+ return if @silent
322
+ console.log "#{@req.method} #{@req.path} - #{Date.now() - @start}ms"
323
+ ```
324
+
325
+ **Note:** `raw` receives the native `Request` object (before parsing). `before` and `after` use `@` to access the context.
326
+
327
+ **How `@` works:** Handlers are called with `this` bound to the context, so `@foo` is `this.foo`. This gives you Sinatra-like magic access to:
328
+ - `@req` — Request object
329
+ - `@json()`, `@text()`, `@html()`, `@redirect()`, `@send()` — Response helpers
330
+ - `@header()` — Response header modifier
331
+ - `@anything` — Custom per-request state
332
+
333
+ **Imports that work anywhere** (via AsyncLocalStorage or Proxy):
334
+ - `read` — Validated request parameters
335
+ - `session` — Session data (if middleware enabled)
336
+ - `env` — `process.env` shortcut (e.g., `env.DATABASE_URL`)
337
+
338
+ ## Context Object
339
+
340
+ Use `@` to access the context directly — no parameter needed:
341
+
342
+ ### Response Helpers
343
+
344
+ ```coffee
345
+ get '/demo' ->
346
+ # JSON response
347
+ @json { data: 'value' }
348
+ @json { data: 'value' }, 201 # With status
349
+ @json { data: 'value' }, 200, { 'X-Custom': 'header' }
350
+
351
+ # Text response
352
+ @text 'Hello'
353
+ @text 'Created', 201
354
+
355
+ # HTML response
356
+ @html '<h1>Hello</h1>'
357
+
358
+ # Redirect
359
+ @redirect '/new-location'
360
+ @redirect '/new-location', 301 # Permanent
361
+
362
+ # Raw body
363
+ @body data, 200, { 'Content-Type': 'application/octet-stream' }
364
+
365
+ # File serving (auto-detected MIME type via Bun.file)
366
+ @send 'public/style.css' # text/css
367
+ @send 'data/export.json', 'application/json' # explicit type
368
+ ```
369
+
370
+ ### Request Helpers
371
+
372
+ ```coffee
373
+ get '/info' ->
374
+ # Path and query parameters — use read() for validation!
375
+ id = read 'id', 'id!'
376
+ q = read 'q'
377
+
378
+ # Headers
379
+ auth = @req.header 'Authorization'
380
+ allHeaders = @req.header()
381
+
382
+ # Body (async)
383
+ json = @req.json!
384
+ text = @req.text!
385
+ form = @req.formData!
386
+ parsed = @req.parseBody!
387
+
388
+ # Raw request
389
+ @req.raw # Native Request object
390
+ @req.method # 'GET', 'POST', etc.
391
+ @req.url # Full URL
392
+ @req.path # Path only
393
+ ```
394
+
395
+ ### Request-Scoped State
396
+
397
+ ```coffee
398
+ # Store data for later middleware/handlers
399
+ use (c, next) ->
400
+ @user = { id: 1, name: 'Alice' }
401
+ @startTime = Date.now()
402
+ await next()
403
+
404
+ get '/profile' ->
405
+ @json @user
406
+ ```
407
+
408
+ ## File Serving
409
+
410
+ ### `@send(path, type?)`
411
+
412
+ Serve a file with auto-detected MIME type. Uses `Bun.file()` internally for
413
+ efficient streaming — the file is never buffered in memory.
414
+
415
+ ```coffee
416
+ # Auto-detected content type (30+ extensions supported)
417
+ get '/css/*' -> @send "css/#{@req.path.slice(5)}"
418
+
419
+ # Explicit content type
420
+ get '/files/*' -> @send "uploads/#{@req.path.slice(7)}", 'application/octet-stream'
421
+
422
+ # SPA fallback — serve index.html for all unmatched routes
423
+ notFound -> @send 'index.html', 'text/html; charset=UTF-8'
424
+ ```
425
+
426
+ ### `mimeType(path)`
427
+
428
+ Exported utility that returns the MIME type for a file path:
429
+
430
+ ```coffee
431
+ import { mimeType } from '@rip-lang/server'
432
+
433
+ mimeType 'style.css' # 'text/css; charset=UTF-8'
434
+ mimeType 'app.js' # 'application/javascript'
435
+ mimeType 'photo.png' # 'image/png'
436
+ mimeType 'data.xyz' # 'application/octet-stream'
437
+ ```
438
+
439
+ ## Error Handling
440
+
441
+ ### Custom Error Handler
442
+
443
+ ```coffee
444
+ import { onError } from '@rip-lang/server'
445
+
446
+ onError (err, c) ->
447
+ console.error 'Error:', err
448
+ c.json { error: err.message }, err.status or 500
449
+ ```
450
+
451
+ ### Custom 404 Handler
452
+
453
+ ```coffee
454
+ import { notFound } from '@rip-lang/server'
455
+
456
+ notFound (c) ->
457
+ c.json { error: 'Not found', path: c.req.path }, 404
458
+ ```
459
+
460
+ ## Server Options
461
+
462
+ ### Basic Server
463
+
464
+ ```coffee
465
+ import { start } from '@rip-lang/server'
466
+
467
+ start port: 3000
468
+ start port: 3000, host: '0.0.0.0'
469
+ ```
470
+
471
+ ### Handler Only (for custom servers)
472
+
473
+ ```coffee
474
+ import { startHandler } from '@rip-lang/server'
475
+
476
+ export default startHandler()
477
+ ```
478
+
479
+ ### App Pattern
480
+
481
+ ```coffee
482
+ import { App, get, post } from '@rip-lang/server'
483
+
484
+ export default App ->
485
+ get '/' -> 'Hello'
486
+ post '/echo' -> read()
487
+ ```
488
+
489
+ ## Context Utilities
490
+
491
+ ### ctx()
492
+
493
+ Get the current request context from anywhere (via AsyncLocalStorage):
494
+
495
+ ```coffee
496
+ import { ctx } from '@rip-lang/server'
497
+
498
+ logRequest = ->
499
+ c = ctx()
500
+ console.log "#{c.req.method} #{c.req.path}" if c
501
+
502
+ get '/demo' ->
503
+ logRequest()
504
+ { ok: true }
505
+ ```
506
+
507
+ ### resetGlobals()
508
+
509
+ Reset all global state (routes, middleware, filters). Useful for testing:
510
+
511
+ ```coffee
512
+ import { resetGlobals, get, start } from '@rip-lang/server'
513
+
514
+ beforeEach ->
515
+ resetGlobals()
516
+
517
+ get '/test' -> { test: true }
518
+ ```
519
+
520
+ ## Utility Functions
521
+
522
+ ### isBlank
523
+
524
+ ```coffee
525
+ import { isBlank } from '@rip-lang/server'
526
+
527
+ isBlank null # true
528
+ isBlank undefined # true
529
+ isBlank '' # true
530
+ isBlank ' ' # true
531
+ isBlank [] # true
532
+ isBlank {} # true
533
+ isBlank false # true
534
+ isBlank 'hello' # false
535
+ isBlank [1, 2] # false
536
+ ```
537
+
538
+ ### toName
539
+
540
+ Advanced name formatting with intelligent capitalization:
541
+
542
+ ```coffee
543
+ import { toName } from '@rip-lang/server'
544
+
545
+ toName 'john doe' # 'John Doe'
546
+ toName 'JANE SMITH' # 'Jane Smith'
547
+ toName "o'brien" # "O'Brien"
548
+ toName 'mcdonald' # 'McDonald'
549
+ toName 'P. o. bOX #44', 'address' # 'PO Box #44'
550
+ toName '123 main st ne', 'address' # '123 Main St NE'
551
+ ```
552
+
553
+ ### toPhone
554
+
555
+ US phone number formatting:
556
+
557
+ ```coffee
558
+ import { toPhone } from '@rip-lang/server'
559
+
560
+ toPhone '5551234567' # '(555) 123-4567'
561
+ toPhone '555-123-4567' # '(555) 123-4567'
562
+ toPhone '555.123.4567 x99' # '(555) 123-4567, ext. 99'
563
+ toPhone '+1 555 123 4567' # '(555) 123-4567'
564
+ ```
565
+
566
+ ## Migration from Hono
567
+
568
+ ### Before (Hono)
569
+
570
+ ```coffee
571
+ import { Hono } from 'hono'
572
+
573
+ app = new Hono()
574
+ app.get '/users/:id', (c) ->
575
+ id = c.req.param 'id'
576
+ c.json { id }
577
+
578
+ export default app
579
+ ```
580
+
581
+ ### After (@rip-lang/server)
582
+
583
+ ```coffee
584
+ import { get, read, startHandler } from '@rip-lang/server'
585
+
586
+ get '/users/:id' ->
587
+ id = read 'id', 'id!'
588
+ { id }
589
+
590
+ export default startHandler()
591
+ ```
592
+
593
+ ### API Compatibility
594
+
595
+ | Hono | @rip-lang/server |
596
+ |------|------------------|
597
+ | `app.get(path, handler)` | `get path, handler` |
598
+ | `app.post(path, handler)` | `post path, handler` |
599
+ | `app.use(middleware)` | `use middleware` |
600
+ | `app.basePath(path)` | `prefix path, -> ...` |
601
+ | `c.json(data)` | `@json(data)` or return `{ data }` |
602
+ | `c.req.param('id')` | `@req.param('id')` or `read 'id'` |
603
+ | `c.req.query('q')` | `@req.query('q')` or `read 'q'` |
604
+
605
+ ## Real-World Example
606
+
607
+ ```coffee
608
+ import { get, post, put, del, use, read, start, before, after, onError } from '@rip-lang/server'
609
+ import { logger } from '@rip-lang/server/middleware'
610
+
611
+ use logger()
612
+
613
+ before ->
614
+ @start = Date.now()
615
+
616
+ after ->
617
+ console.log "#{@req.method} #{@req.path} - #{Date.now() - @start}ms"
618
+
619
+ onError (err) ->
620
+ @json { error: err.message }, err.status or 500
621
+
622
+ get '/' ->
623
+ { name: 'My API', version: '1.0' }
624
+
625
+ get '/users' ->
626
+ page = read 'page', 'int', [1, 100]
627
+ limit = read 'limit', 'int', [1, 50]
628
+ users = db.listUsers! page or 1, limit or 10
629
+ { users, page, limit }
630
+
631
+ get '/users/:id' ->
632
+ id = read 'id', 'id!'
633
+ user = db.getUser!(id) or throw { message: 'User not found', status: 404 }
634
+ { user }
635
+
636
+ post '/users' ->
637
+ email = read 'email', 'email!'
638
+ name = read 'name', 'string', [1, 100]
639
+ phone = read 'phone', 'phone'
640
+ user = db.createUser! { email, name, phone }
641
+ { user, created: true }
642
+
643
+ put '/users/:id' ->
644
+ id = read 'id', 'id!'
645
+ email = read 'email', 'email'
646
+ name = read 'name', 'string', [1, 100]
647
+ user = db.updateUser! id, { email, name }
648
+ { user, updated: true }
649
+
650
+ del '/users/:id' ->
651
+ id = read 'id', 'id!'
652
+ db.deleteUser!(id)
653
+ { deleted: true }
654
+
655
+ start port: 3000
656
+ ```