create-acrylblends 0.0.0-stage → 0.1.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.
Files changed (106) hide show
  1. package/bin/create-acrylblends.js +4 -0
  2. package/dist/index.d.ts +56 -0
  3. package/dist/index.js +274 -0
  4. package/dist/index.js.map +1 -0
  5. package/docs/framework/LICENSE +23 -0
  6. package/docs/framework/README.md +63 -0
  7. package/docs/framework/UPSTREAM.md +3 -0
  8. package/docs/framework/agents/index.md +24 -0
  9. package/docs/framework/api/acryl.md +259 -0
  10. package/docs/framework/api/context.md +527 -0
  11. package/docs/framework/api/exception.md +80 -0
  12. package/docs/framework/api/index.md +12 -0
  13. package/docs/framework/api/presets.md +65 -0
  14. package/docs/framework/api/request.md +396 -0
  15. package/docs/framework/api/routing.md +303 -0
  16. package/docs/framework/api/summary.md +140 -0
  17. package/docs/framework/concepts/benchmarks.md +26 -0
  18. package/docs/framework/concepts/developer-experience.md +5 -0
  19. package/docs/framework/concepts/middleware.md +21 -0
  20. package/docs/framework/concepts/motivation.md +14 -0
  21. package/docs/framework/concepts/routers.md +63 -0
  22. package/docs/framework/concepts/stacks.md +235 -0
  23. package/docs/framework/concepts/web-standard.md +37 -0
  24. package/docs/framework/cordis/acryl-in-a-cordis-app.md +146 -0
  25. package/docs/framework/cordis/concepts.md +105 -0
  26. package/docs/framework/cordis/middleware.md +8 -0
  27. package/docs/framework/cordis/migration-from-0-3.md +111 -0
  28. package/docs/framework/cordis/migration-from-0-4.md +14 -0
  29. package/docs/framework/cordis/migration-from-dsh-webserver.md +103 -0
  30. package/docs/framework/cordis/mounting.md +85 -0
  31. package/docs/framework/cordis/node-server.md +75 -0
  32. package/docs/framework/cordis/plugin-lifecycle.md +111 -0
  33. package/docs/framework/cordis/surfaces.md +71 -0
  34. package/docs/framework/cordis/transport-and-cancellation.md +57 -0
  35. package/docs/framework/cordis/validation.md +135 -0
  36. package/docs/framework/getting-started/basic.md +196 -0
  37. package/docs/framework/getting-started/bun.md +221 -0
  38. package/docs/framework/getting-started/nodejs.md +344 -0
  39. package/docs/framework/getting-started/other-runtimes.md +23 -0
  40. package/docs/framework/guides/best-practices.md +231 -0
  41. package/docs/framework/guides/create-acrylblends.md +79 -0
  42. package/docs/framework/guides/examples.md +3 -0
  43. package/docs/framework/guides/faq.md +19 -0
  44. package/docs/framework/guides/helpers.md +33 -0
  45. package/docs/framework/guides/jsx-dom.md +299 -0
  46. package/docs/framework/guides/jsx.md +440 -0
  47. package/docs/framework/guides/middleware.md +253 -0
  48. package/docs/framework/guides/rpc.md +886 -0
  49. package/docs/framework/guides/testing.md +133 -0
  50. package/docs/framework/guides/validation.md +383 -0
  51. package/docs/framework/helpers/accepts.md +60 -0
  52. package/docs/framework/helpers/adapter.md +87 -0
  53. package/docs/framework/helpers/conninfo.md +112 -0
  54. package/docs/framework/helpers/cookie.md +232 -0
  55. package/docs/framework/helpers/css.md +285 -0
  56. package/docs/framework/helpers/dev.md +67 -0
  57. package/docs/framework/helpers/factory.md +180 -0
  58. package/docs/framework/helpers/html.md +123 -0
  59. package/docs/framework/helpers/jwt.md +184 -0
  60. package/docs/framework/helpers/proxy.md +118 -0
  61. package/docs/framework/helpers/route.md +143 -0
  62. package/docs/framework/helpers/ssg.md +393 -0
  63. package/docs/framework/helpers/streaming.md +119 -0
  64. package/docs/framework/helpers/testing.md +101 -0
  65. package/docs/framework/helpers/websocket.md +215 -0
  66. package/docs/framework/middleware/builtin/basic-auth.md +165 -0
  67. package/docs/framework/middleware/builtin/bearer-auth.md +138 -0
  68. package/docs/framework/middleware/builtin/body-limit.md +71 -0
  69. package/docs/framework/middleware/builtin/cache.md +122 -0
  70. package/docs/framework/middleware/builtin/combine.md +94 -0
  71. package/docs/framework/middleware/builtin/compress.md +59 -0
  72. package/docs/framework/middleware/builtin/context-storage.md +78 -0
  73. package/docs/framework/middleware/builtin/cors.md +135 -0
  74. package/docs/framework/middleware/builtin/csrf.md +105 -0
  75. package/docs/framework/middleware/builtin/etag.md +54 -0
  76. package/docs/framework/middleware/builtin/ip-restriction.md +69 -0
  77. package/docs/framework/middleware/builtin/jsx-renderer.md +286 -0
  78. package/docs/framework/middleware/builtin/jwk.md +192 -0
  79. package/docs/framework/middleware/builtin/jwt.md +145 -0
  80. package/docs/framework/middleware/builtin/language.md +232 -0
  81. package/docs/framework/middleware/builtin/logger.md +74 -0
  82. package/docs/framework/middleware/builtin/loopback-only.md +31 -0
  83. package/docs/framework/middleware/builtin/method-not-allowed.md +48 -0
  84. package/docs/framework/middleware/builtin/method-override.md +80 -0
  85. package/docs/framework/middleware/builtin/mount.md +106 -0
  86. package/docs/framework/middleware/builtin/pretty-json.md +53 -0
  87. package/docs/framework/middleware/builtin/request-id.md +94 -0
  88. package/docs/framework/middleware/builtin/secure-headers.md +247 -0
  89. package/docs/framework/middleware/builtin/timeout.md +98 -0
  90. package/docs/framework/middleware/builtin/timing.md +101 -0
  91. package/docs/framework/middleware/builtin/trailing-slash.md +74 -0
  92. package/docs/framework/middleware/third-party.md +4 -0
  93. package/docs/framework/upstream.json +9 -0
  94. package/package.json +47 -3
  95. package/templates/bun/gitignore +1 -0
  96. package/templates/bun/package.json +19 -0
  97. package/templates/bun/src/index.ts +7 -0
  98. package/templates/bun/test/app.test.ts +8 -0
  99. package/templates/bun/tsconfig.json +15 -0
  100. package/templates/nodejs/gitignore +1 -0
  101. package/templates/nodejs/package.json +22 -0
  102. package/templates/nodejs/src/app.ts +7 -0
  103. package/templates/nodejs/src/index.ts +9 -0
  104. package/templates/nodejs/test/app.test.ts +9 -0
  105. package/templates/nodejs/tsconfig.json +16 -0
  106. package/README.md +0 -3
@@ -0,0 +1,111 @@
1
+ # Plugin lifecycle and disposal
2
+
3
+ Cordis gives every framework registration an owner and a lifetime. This is the main difference between using the application API on its
4
+ own and using it as part of ACRYL.
5
+
6
+ ## Every registration has an owner
7
+
8
+ ```ts
9
+ export const name = 'todo'
10
+ export const inject = ['router']
11
+
12
+ export function apply(ctx: Context): void {
13
+ ctx.router.route('/todo', createTodoApp())
14
+ }
15
+ ```
16
+
17
+ `route`, `onError` and `notFound` register themselves as effects of the plugin that called them, so there is nothing to wrap: when the
18
+ plugin's fiber unloads, its mount goes away, and a reload mounts it again without duplicates. `route` returns a function that removes the
19
+ mount sooner. Never register routes as a side effect of importing a module.
20
+
21
+ Resources an app uses (timers, watchers, connections, subprocesses) belong to one `ctx.effect()` of the plugin, whose disposer releases
22
+ them:
23
+
24
+ ```ts
25
+ export function apply(ctx: Context): void {
26
+ const events = new EventEmitter()
27
+ ctx.effect(() => {
28
+ const timer = setInterval(() => events.emit('tick'), 1000)
29
+ return () => clearInterval(timer)
30
+ }, 'todo: ticker')
31
+ ctx.router.route('/todo', createTodoApp(events))
32
+ }
33
+ ```
34
+
35
+ ## Live composition
36
+
37
+ The router is built for a host whose plugin set changes while it runs. Mounts are added and removed, and each request after a change is
38
+ served by an app composed from the mounts that are live at that time; a request already running keeps the app it started with. The core's
39
+ route tables only grow, so the router rebuilds the composition instead of editing it. Application code does not manage this.
40
+
41
+ A mount is a snapshot of the app's routes at the `route` call. Build the app, then mount it.
42
+
43
+ ## Missing providers
44
+
45
+ A plugin that injects `router` stays PENDING while no router exists. That is a normal Cordis state, not an error: when a router appears the
46
+ plugin activates and mounts. When the router is replaced, the plugin unloads and activates again against the new one, so it must not keep
47
+ the old service object anywhere else.
48
+
49
+ ## Disposal sequence
50
+
51
+ When a plugin that owns a mount unloads:
52
+
53
+ 1. The mount leaves the composition, so new requests no longer reach it.
54
+ 2. The requests its code is still serving end: a request inside one of its handlers or middleware (before `next()` returned), and a
55
+ response body one of its handlers produced. Their signals abort (`c.req.raw.signal`) and their bodies are cancelled, which runs the
56
+ stream helpers' `onAbort`.
57
+ 3. The unload waits for those requests, their bodies and their `c.executionCtx.waitUntil` work, bounded by the router's `disposeGraceMs`.
58
+
59
+ A request the plugin's middleware already passed on to another plugin's handler is not touched. When the router itself is disposed, the
60
+ same happens to every running request, and new requests are answered with 503.
61
+
62
+ Handlers get a chance to release their resources, nothing a plugin started outlives it, and disposal cannot hang forever.
63
+
64
+ ## Services and the shadow context
65
+
66
+ A plugin reaches a service through its own view of it: `ctx.router` in one plugin and in another are two different objects over one
67
+ service. Two facts follow, each with a test as proof.
68
+
69
+ **Fields are shared.** Reading or assigning a service's field through any plugin's view reads or writes the one instance, so every caller
70
+ sees the same value (`plugins/acryl-router/tests/shadow-state.spec.ts`). Keeping changing state in one object, as the router does, is a
71
+ readability choice, not a requirement.
72
+
73
+ **`this.ctx` inside a method is the caller's view, not the service's own context.** It is what lets `route` tie a mount to the plugin
74
+ that called it. It is the wrong context for starting something that should belong to the service: a plugin started with
75
+ `this.ctx.plugin(...)` from a method resolves services as the service's own fiber and fails with "cannot get property router without
76
+ inject". Keep the context the constructor received and use that:
77
+
78
+ ```ts
79
+ class Installer extends Service {
80
+ private readonly owner: Context
81
+
82
+ constructor(ctx: Context) {
83
+ super(ctx, 'installer')
84
+ this.owner = ctx // the service's own context
85
+ }
86
+
87
+ async install(plugin: Plugin): Promise<void> {
88
+ await this.owner.plugin(plugin) // right: behaves like a plugin mounted at startup
89
+ // await this.ctx.plugin(plugin) // wrong: the caller's view, see above
90
+ }
91
+ }
92
+ ```
93
+
94
+ The proof is `plugins/acryl-router/tests/live-install.spec.ts`, which runs both forms against a live router. This sits next to the general
95
+ rule: a plugin started with `this.ctx.plugin(...)` in `apply` is a child of the plugin and unloads with it.
96
+
97
+ ## Module state
98
+
99
+ Do not hide mutable state in module globals. Cordis can reload a plugin and run several surfaces from one host, and a module global
100
+ outlives the plugin that wrote it.
101
+
102
+ ## What to test
103
+
104
+ Test a route-owning plugin through real Cordis activation for:
105
+
106
+ - activation when the router is present, and PENDING until it appears;
107
+ - route removal after the plugin unloads, and a clean mount after reload, without duplicates over repeated cycles;
108
+ - router replacement without stale service references;
109
+ - disposal while a response is still open.
110
+
111
+ `app.request` tests prove handler behavior. Only activation tests prove ownership.
@@ -0,0 +1,71 @@
1
+ # Surfaces and adapters
2
+
3
+ An ACRYL application is written once and reached through the surface that fits the user. Surfaces provide transport and presentation;
4
+ plugins provide capabilities.
5
+
6
+ ## Surface model
7
+
8
+ | Surface | How requests arrive | Typical use |
9
+ | --- | --- | --- |
10
+ | Web | HTTP to the ACRYL Host's web server, through `acryl-router-http` | browser applications and APIs |
11
+ | Desktop | the same Host web server on a loopback port, opened in the Electron window | local-first applications |
12
+ | Terminal UI | in process: `ctx.router.request(...)` or `ctx.router.fetch(...)` | operator tools, agents, terminal workflows |
13
+ | Standalone process | HTTP through `acryl-node-server` | an app that runs without the Host |
14
+ | Tests | `ctx.router.request(...)` or `app.request(...)` | handler and contract tests |
15
+
16
+ The handler does not know which row supplied the request. When it needs a transport fact it reads the connection from the context
17
+ (`connectionOf(c)`), which is empty for an in-process call.
18
+
19
+ ## Web
20
+
21
+ The Host owns the listening socket, the address and process shutdown. A plugin mounts its app on the router, then asks the adapter to
22
+ serve the prefix over HTTP:
23
+
24
+ ```ts
25
+ ctx.router.route('/todo', app)
26
+ ctx.inject(['routerHttp'], inner => { inner.routerHttp.mount('/todo') })
27
+ ```
28
+
29
+ Only prefixes asked for this way are served over HTTP. A prefix the Host's web server already serves cannot be claimed: `mount` throws,
30
+ so a plugin cannot take over a Host route by mounting on it.
31
+
32
+ ## Desktop
33
+
34
+ Desktop runs the same Host inside Electron, and its window talks to the Host's web server on a loopback port. A plugin's routes therefore
35
+ serve Desktop exactly as they serve Web, with no Desktop-specific code. Guard local control routes with `loopbackOnly` (see
36
+ [Middleware](../concepts/middleware.md#loopbackonly)).
37
+
38
+ A transport without a port, handing `ctx.router.fetch` to Electron's `protocol.handle`, needs no change to plugins and is a later task
39
+ (`specs/047-framework-port/plan.md` T014).
40
+
41
+ Desktop capabilities such as windows, files and native dialogs belong in declared Cordis services. A handler calls the service; it does not
42
+ import Electron.
43
+
44
+ ## Terminal UI
45
+
46
+ A terminal surface needs no server. It calls the composed router in process:
47
+
48
+ ```ts
49
+ const response = await ctx.router.request('/todo/todos')
50
+ const todos = await response.json()
51
+ ```
52
+
53
+ The `routerHttp` service does not exist there, so the plugin's `ctx.inject(['routerHttp'], ...)` simply never runs. Check the services a
54
+ host declares instead of assuming a network route exists.
55
+
56
+ ## What belongs where
57
+
58
+ | Concern | Owner |
59
+ | --- | --- |
60
+ | Route handlers and middleware | the feature plugin's `Acryl` |
61
+ | Mount path and lifetime | the feature plugin, through `ctx.router.route` |
62
+ | Socket and port | the Host (`acryl-router-http`) or `acryl-node-server` |
63
+ | Terminal rendering and input | the terminal surface |
64
+ | Shared domain behavior | a domain service every surface uses |
65
+ | Shared reactive state | `acryl-state` or an application service |
66
+
67
+ ## Loopback-only routes
68
+
69
+ Some routes exist only for the local host, such as callbacks and control endpoints. Guard them with `loopbackOnly` at the framework
70
+ boundary; do not rely on an obscure path or a missing link as access control. Loopback restriction is separate from authentication: a
71
+ local route that does privileged work still needs the host's authorization policy.
@@ -0,0 +1,57 @@
1
+ # Transport facts and cancellation
2
+
3
+ `c.env` carries what the caller of the router supplied for this request. Under the router that is `{ connection }`: the socket's peer
4
+ address and the real local port, filled by the HTTP adapter and empty for an in-process call. Transport facts never travel in headers.
5
+
6
+ ```ts
7
+ import { Acryl } from 'acrylblends'
8
+ import { connectionOf, type RouterEnv } from 'acrylblends/conninfo'
9
+ import { getConnInfo } from 'acrylblends/conninfo'
10
+
11
+ const app = new Acryl<RouterEnv>() // types c.env.connection
12
+ app.get('/peer', c => c.json({
13
+ connection: connectionOf(c), // { remoteAddress?, localPort? }
14
+ info: getConnInfo(c), // the same peer in the core's conninfo shape, for middleware such as ipRestriction
15
+ }))
16
+ ```
17
+
18
+ The same handler then runs over HTTP, in process or in a test, and reads an empty connection where there is no socket. Guards such as
19
+ `loopbackOnly` refuse an empty one unless told otherwise.
20
+
21
+ ## Cancellation and background work
22
+
23
+ - `c.req.raw.signal` aborts when the caller goes away, when the plugin whose code is serving the request unloads, and when the router is
24
+ disposed. Long work
25
+ should pass it on (`fetch(url, { signal: c.req.raw.signal })`) or check it.
26
+ - `c.executionCtx.waitUntil(promise)` keeps the request in flight until the promise settles, for work that continues after the response
27
+ is returned.
28
+
29
+ Both belong to the current request, and both end with their owner: see [Plugin lifecycle](./plugin-lifecycle.md#disposal-sequence).
30
+
31
+ ## Errors and control flow
32
+
33
+ Return a response for expected failures, or throw an `HTTPException`, which becomes its own response:
34
+
35
+ ```ts
36
+ import { HTTPException } from 'acrylblends/http-exception'
37
+
38
+ if (!todo) return c.json({ error: 'todo_not_found' }, 404)
39
+ if (!allowed) throw new HTTPException(403, { message: 'not yours' })
40
+ ```
41
+
42
+ Anything else that is thrown goes to the app's `onError`, or to the router's when the app has none. A middleware's `await next()` never
43
+ throws: the error has already become a response by then, and the error itself is in `c.error`:
44
+
45
+ ```ts
46
+ app.use(async (c, next) => {
47
+ await next()
48
+ if (c.error) report(c.error)
49
+ })
50
+ ```
51
+
52
+ ## Context rules
53
+
54
+ - Treat the context as request-local.
55
+ - Do not retain it after the handler returns.
56
+ - Do not put sockets, server objects or Cordis internals into context values for other plugins to discover accidentally.
57
+ - Put cross-plugin capabilities in a declared Cordis service, not in `c.set`.
@@ -0,0 +1,135 @@
1
+ # Validation
2
+
3
+ Validate untrusted input at the route boundary. `schemaValidator` takes any [Standard Schema](https://standardschema.dev): Schemastery
4
+ (the schema library Cordis already uses for plugin config), Zod and Valibot all implement it, so a route needs no extra dependency.
5
+
6
+ ## Validate JSON
7
+
8
+ ```ts
9
+ import z from '@deepseek-ai/schemastery'
10
+ import { Acryl } from 'acrylblends'
11
+ import { schemaValidator } from 'acrylblends/schema-validator'
12
+
13
+ const NewTodo = z.object({
14
+ title: z.string().required(),
15
+ done: z.boolean().default(false),
16
+ })
17
+
18
+ const app = new Acryl().post('/todos', schemaValidator('json', NewTodo), c => {
19
+ const todo = c.req.valid('json') // typed by the schema's output
20
+ return c.json({ todo }, 201)
21
+ })
22
+ ```
23
+
24
+ A validated route has three parts: a schema for one input target, the validator middleware for that target, and a handler that reads
25
+ `c.req.valid(target)` instead of the raw body.
26
+
27
+ ## Failures
28
+
29
+ When the schema reports issues the handler does not run and the answer is 400 with `Content-Type: application/json` and exactly this
30
+ shape:
31
+
32
+ ```ts
33
+ { error: `invalid ${target}`, issues: Array<{ message: string, path: string }> }
34
+ ```
35
+
36
+ ```json
37
+ { "error": "invalid json", "issues": [{ "message": "title must be 1 to 200 characters", "path": "title" }] }
38
+ ```
39
+
40
+ - `error` names the target: `invalid json`, `invalid query`, `invalid param`, ...
41
+ - There is one entry per issue the schema reported, in its order, with the schema's own `message`.
42
+ - `path` is the issue's path joined with dots: `["items", 0, "title"]` becomes `"items.0.title"`, and a path segment given as
43
+ `{ key }` (as Standard Schema allows) contributes its `key`.
44
+ - An issue without a path, which a hand-written Standard Schema often reports for a whole-value check, gets `"path": ""`.
45
+ `{ issues: [{ message: 'not allowed' }] }` answers `{ "error": "invalid json", "issues": [{ "message": "not allowed", "path": "" }] }`.
46
+ Give issues a path when a client should point at a field.
47
+
48
+ ### A JSON body without a JSON content type
49
+
50
+ A `json` target reads the body only when the request's `Content-Type` is JSON (`application/json`, with or without parameters such as
51
+ `; charset=utf-8`, or `application/*+json`). Any other content type, or none, is not an error by itself: the schema sees `{}`. So a
52
+ client that forgets the header gets a misleading field error for a field it did send:
53
+
54
+ ```sh
55
+ curl -X POST -d '{"title":"milk"}' http://127.0.0.1:<port>/todo/api # no content type: curl sends a form type
56
+ # 400 {"error":"invalid json","issues":[{"message":"title must be 1 to 200 characters","path":"title"}]}
57
+ ```
58
+
59
+ If you see a field error for a field the request clearly carries, check the request's `Content-Type` first. A route that wants to say so
60
+ explicitly can refuse other content types before the validator:
61
+
62
+ ```ts
63
+ import type { MiddlewareHandler } from 'acrylblends'
64
+
65
+ const jsonOnly: MiddlewareHandler = async (c, next) => {
66
+ // The same types the json target reads: application/json and application/*+json, parameters allowed.
67
+ const type = c.req.header('content-type')?.split(';', 1)[0]?.trim() ?? ''
68
+ if (!/^application\/([a-z.-]+\+)?json$/i.test(type)) return c.json({ error: 'content type must be JSON' }, 415)
69
+ await next()
70
+ }
71
+
72
+ app.post('/todos', jsonOnly, schemaValidator('json', NewTodo), createTodo)
73
+ ```
74
+
75
+ ### A body that is not JSON
76
+
77
+ A JSON content type with a body that does not parse fails before the schema runs: the answer is 400 with the text body
78
+ `Malformed JSON in request body` (a thrown `HTTPException`, so an `onError` handler can reshape it). The `form` target does the same for
79
+ a form body that does not parse, with the text `Malformed FormData request.`
80
+
81
+ Keep the public failure shape consistent across the application and do not leak internals in messages. An application that needs a
82
+ different body writes its own check with the core `validator` (below). Valid input is not proof that the caller may use it:
83
+ authentication and authorization stay separate checks.
84
+
85
+ ## Targets
86
+
87
+ | Target | Read from |
88
+ | --- | --- |
89
+ | `json` | the JSON body |
90
+ | `form` | a `multipart/form-data` or `application/x-www-form-urlencoded` body |
91
+ | `query` | the query string |
92
+ | `param` | route parameters |
93
+ | `header` | request headers (names in lowercase) |
94
+ | `cookie` | cookies |
95
+
96
+ Validate only the targets the route uses, and chain one validator per target:
97
+
98
+ ```ts
99
+ app.get('/todos/:id', schemaValidator('param', z.object({ id: z.natural() })), schemaValidator('query', Paging), handler)
100
+ ```
101
+
102
+ ## The core validator
103
+
104
+ `validator` (from `acrylblends/validator`) takes a function instead of a schema. Return the value the handler should see, or a
105
+ response to refuse:
106
+
107
+ ```ts
108
+ import { validator } from 'acrylblends/validator'
109
+
110
+ app.post('/posts', validator('form', (value, c) => {
111
+ const body = value['body']
112
+ if (typeof body !== 'string') return c.text('Invalid!', 400)
113
+ return { body }
114
+ }), c => c.json({ body: c.req.valid('form').body }, 201))
115
+ ```
116
+
117
+ ## Limit the body first
118
+
119
+ Validation reads the whole body. Put `bodyLimit` in front of a validator on any route open to untrusted callers:
120
+
121
+ ```ts
122
+ import { bodyLimit } from 'acrylblends/body-limit'
123
+
124
+ app.post('/todos', bodyLimit({ maxSize: 16 * 1024 }), schemaValidator('json', NewTodo), createTodo)
125
+ ```
126
+
127
+ ## Typed clients
128
+
129
+ The validated input types flow into the route types, so the typed `testClient` and `acrylblends/client` check the request a caller sends.
130
+ See [Helpers](../guides/helpers.md#testing).
131
+
132
+ ## What not to validate twice
133
+
134
+ Validate shape at the HTTP boundary, then enforce domain rules in the domain service. The route checks that `title` is a string; the
135
+ todo service decides whether that title is allowed.
@@ -0,0 +1,196 @@
1
+ # Getting Started
2
+
3
+ Using Acryl is easy. You set up the project, write code, develop with a local server, and run it. The same application code works on every verified runtime, with a different
4
+ entry point for each. This page shows the basic usage.
5
+
6
+ ## Starter
7
+
8
+ Starter templates are available for each runtime. Use the "create-acrylblends" command.
9
+
10
+ **npm**
11
+
12
+ ```sh
13
+ npm create acrylblends@latest my-app
14
+ ```
15
+
16
+ **yarn**
17
+
18
+ ```sh
19
+ yarn create acrylblends my-app
20
+ ```
21
+
22
+ **pnpm**
23
+
24
+ ```sh
25
+ pnpm create acrylblends@latest my-app
26
+ ```
27
+
28
+ **bun**
29
+
30
+ ```sh
31
+ bun create acrylblends@latest my-app
32
+ ```
33
+
34
+ You are asked which template you want:
35
+
36
+ ```
37
+ ? Which template do you want to use?
38
+ ❯ nodejs
39
+ bun (experimental)
40
+ acryl-app
41
+ ```
42
+
43
+ `nodejs` and `bun` (experimental) are a minimal Acryl application for that runtime. `acryl-app` is a Cordis app built from plugins, with an agent that can extend it while it
44
+ runs (see [Acryl in a Cordis app](../cordis/acryl-in-a-cordis-app.md)). Pass `--template` to skip the question, see [create-acrylblends](../guides/create-acrylblends.md).
45
+
46
+ The template is created in `my-app`: go to it, install the dependencies and start the local server.
47
+
48
+ ```sh
49
+ cd my-app
50
+ npm install
51
+ npm run dev
52
+ ```
53
+
54
+ ## Hello World
55
+
56
+ Write your first application in `src/index.ts`. The example below is a starter Acryl application.
57
+
58
+ The `import` and the final `export default` are the same on every runtime; what differs is how the runtime starts the app (see [Node.js](nodejs.md) and [Bun](bun.md)).
59
+
60
+ ```ts twoslash
61
+ import { Acryl } from 'acrylblends'
62
+
63
+ const app = new Acryl()
64
+
65
+ app.get('/', (c) => {
66
+ return c.text('Hello Acryl!')
67
+ })
68
+
69
+ export default app
70
+ ```
71
+
72
+ Start the development server and open the address it prints.
73
+
74
+ ## Return JSON
75
+
76
+ Returning JSON is also easy. The following handles a GET request to `/api/hello` and returns an `application/json` response.
77
+
78
+ ```ts twoslash
79
+ import { Acryl } from 'acrylblends'
80
+
81
+ const app = new Acryl()
82
+
83
+ app.get('/api/hello', (c) => {
84
+ return c.json({
85
+ ok: true,
86
+ message: 'Hello Acryl!',
87
+ })
88
+ })
89
+ ```
90
+
91
+ ## Request and Response
92
+
93
+ Getting a path parameter, a URL query value, and adding a response header is written as follows.
94
+
95
+ ```ts twoslash
96
+ import { Acryl } from 'acrylblends'
97
+
98
+ const app = new Acryl()
99
+
100
+ app.get('/posts/:id', (c) => {
101
+ const page = c.req.query('page')
102
+ const id = c.req.param('id')
103
+ c.header('X-Message', 'Hi!')
104
+ return c.text(`You want to see ${page} of ${id}`)
105
+ })
106
+ ```
107
+
108
+ POST, PUT and DELETE are handled the same way as GET.
109
+
110
+ ```ts twoslash
111
+ import { Acryl } from 'acrylblends'
112
+
113
+ const app = new Acryl()
114
+
115
+ app.post('/posts', (c) => c.text('Created!', 201))
116
+ app.delete('/posts/:id', (c) => c.text(`${c.req.param('id')} is deleted!`))
117
+ ```
118
+
119
+ ## Return HTML
120
+
121
+ Write HTML with [the html helper](../helpers/html.md):
122
+
123
+ ```ts twoslash
124
+ import { Acryl } from 'acrylblends'
125
+ import { html } from 'acrylblends/html'
126
+
127
+ const app = new Acryl()
128
+
129
+ app.get('/', (c) => {
130
+ const name = 'Acryl'
131
+ return c.html(html`<!doctype html><h1>Hello ${name}!</h1>`)
132
+ })
133
+ ```
134
+
135
+ [JSX](../guides/jsx.md) is not yet available.
136
+
137
+ ## Return raw Response
138
+
139
+ You can also return a raw Web Standard `Response`.
140
+
141
+ ```ts twoslash
142
+ import { Acryl } from 'acrylblends'
143
+
144
+ const app = new Acryl()
145
+
146
+ app.get('/', () => {
147
+ return new Response('Good morning!')
148
+ })
149
+ ```
150
+
151
+ ## Using Middleware
152
+
153
+ Middleware can do the hard work for you. For example, add Basic Authentication.
154
+
155
+ ```ts twoslash
156
+ import { Acryl } from 'acrylblends'
157
+ import { basicAuth } from 'acrylblends/basic-auth'
158
+
159
+ const app = new Acryl()
160
+
161
+ app.use(
162
+ '/admin/*',
163
+ basicAuth({
164
+ username: 'admin',
165
+ password: 'secret',
166
+ })
167
+ )
168
+
169
+ app.get('/admin', (c) => {
170
+ return c.text('You are authorized!')
171
+ })
172
+ ```
173
+
174
+ There are useful middleware included, such as Bearer and JWT authentication, CORS and ETag. See [Built-in middleware](../middleware/builtin/basic-auth.md).
175
+
176
+ ## Testing
177
+
178
+ An app is a function from `Request` to `Response`, so a test needs no server:
179
+
180
+ ```ts twoslash
181
+ import { Acryl } from 'acrylblends'
182
+
183
+ const app = new Acryl()
184
+ app.get('/', (c) => c.text('Hello Acryl!'))
185
+
186
+ const response = await app.request('/')
187
+ console.log(response.status, await response.text())
188
+ ```
189
+
190
+ See [Testing](../guides/testing.md).
191
+
192
+ ## Next
193
+
194
+ - [Node.js](nodejs.md) and [Bun](bun.md) (experimental)
195
+ - [Routing](../api/routing.md), [Context](../api/context.md) and [Request](../api/request.md)
196
+ - [Validation](../guides/validation.md)