@alxia/core 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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +389 -2
  3. package/dist/app/alxia.d.ts +294 -0
  4. package/dist/app/alxia.d.ts.map +1 -0
  5. package/dist/app/chain.d.ts +23 -0
  6. package/dist/app/chain.d.ts.map +1 -0
  7. package/dist/app/define-plugin.d.ts +21 -0
  8. package/dist/app/define-plugin.d.ts.map +1 -0
  9. package/dist/app/definition.d.ts +79 -0
  10. package/dist/app/definition.d.ts.map +1 -0
  11. package/dist/app/pipeline.d.ts +4 -0
  12. package/dist/app/pipeline.d.ts.map +1 -0
  13. package/dist/app/route-operation.d.ts +35 -0
  14. package/dist/app/route-operation.d.ts.map +1 -0
  15. package/dist/app/send.d.ts +22 -0
  16. package/dist/app/send.d.ts.map +1 -0
  17. package/dist/app/socket.d.ts +17 -0
  18. package/dist/app/socket.d.ts.map +1 -0
  19. package/dist/app/types/common.d.ts +6 -0
  20. package/dist/app/types/common.d.ts.map +1 -0
  21. package/dist/app/types/context.d.ts +52 -0
  22. package/dist/app/types/context.d.ts.map +1 -0
  23. package/dist/app/types/index.d.ts +13 -0
  24. package/dist/app/types/index.d.ts.map +1 -0
  25. package/dist/app/types/requires.d.ts +87 -0
  26. package/dist/app/types/requires.d.ts.map +1 -0
  27. package/dist/app/types/route-table.d.ts +61 -0
  28. package/dist/app/types/route-table.d.ts.map +1 -0
  29. package/dist/app/types/schema.d.ts +52 -0
  30. package/dist/app/types/schema.d.ts.map +1 -0
  31. package/dist/app/types/typed-reply.d.ts +32 -0
  32. package/dist/app/types/typed-reply.d.ts.map +1 -0
  33. package/dist/app/types/valid-schema.d.ts +33 -0
  34. package/dist/app/types/valid-schema.d.ts.map +1 -0
  35. package/dist/errors/errors.d.ts +49 -0
  36. package/dist/errors/errors.d.ts.map +1 -0
  37. package/dist/index.d.ts +20 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +1477 -0
  40. package/dist/index.js.map +28 -0
  41. package/dist/reply/headers.d.ts +12 -0
  42. package/dist/reply/headers.d.ts.map +1 -0
  43. package/dist/reply/reply.d.ts +68 -0
  44. package/dist/reply/reply.d.ts.map +1 -0
  45. package/dist/request/read.d.ts +31 -0
  46. package/dist/request/read.d.ts.map +1 -0
  47. package/dist/router/compile.d.ts +39 -0
  48. package/dist/router/compile.d.ts.map +1 -0
  49. package/dist/router/paths.d.ts +32 -0
  50. package/dist/router/paths.d.ts.map +1 -0
  51. package/dist/router/router.d.ts +45 -0
  52. package/dist/router/router.d.ts.map +1 -0
  53. package/dist/schema/standard-schema.d.ts +46 -0
  54. package/dist/schema/standard-schema.d.ts.map +1 -0
  55. package/dist/sse/event-stream.d.ts +31 -0
  56. package/dist/sse/event-stream.d.ts.map +1 -0
  57. package/dist/static/conditional.d.ts +18 -0
  58. package/dist/static/conditional.d.ts.map +1 -0
  59. package/dist/static/send.d.ts +20 -0
  60. package/dist/static/send.d.ts.map +1 -0
  61. package/dist/static/serve.d.ts +8 -0
  62. package/dist/static/serve.d.ts.map +1 -0
  63. package/dist/static/types.d.ts +59 -0
  64. package/dist/static/types.d.ts.map +1 -0
  65. package/dist/types/json.d.ts +17 -0
  66. package/dist/types/json.d.ts.map +1 -0
  67. package/dist/types/path.d.ts +20 -0
  68. package/dist/types/path.d.ts.map +1 -0
  69. package/dist/types/status.d.ts +10 -0
  70. package/dist/types/status.d.ts.map +1 -0
  71. package/dist/ws/types.d.ts +74 -0
  72. package/dist/ws/types.d.ts.map +1 -0
  73. package/docs/README.md +23 -0
  74. package/docs/guide/getting-started.md +135 -0
  75. package/docs/guide/groups-and-plugins.md +212 -0
  76. package/docs/guide/hooks.md +284 -0
  77. package/docs/guide/replies.md +296 -0
  78. package/docs/guide/routes.md +429 -0
  79. package/docs/guide/server-sent-events.md +153 -0
  80. package/docs/guide/serving.md +174 -0
  81. package/docs/guide/static-files.md +212 -0
  82. package/docs/guide/types.md +185 -0
  83. package/docs/guide/websockets.md +196 -0
  84. package/docs/guide/writing-a-plugin.md +259 -0
  85. package/docs/roadmap.md +102 -0
  86. package/docs/troubleshooting.md +957 -0
  87. package/package.json +49 -5
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steve Tsala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,390 @@
1
- # Temporary Holding Version
1
+ # @alxia/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An HTTP framework for [Bun](https://bun.sh), type-safe from the request to
4
+ the client that calls it, with **no dependency**. Each route declares what it
5
+ reads and what it answers with any [Standard Schema](https://standardschema.dev)
6
+ — Zod, Valibot, ArkType, or one written by hand — and the types follow: the
7
+ handler reads validated values, can only answer what it declared, and the
8
+ app's type is the contract [`@alxia/client`](https://www.npmjs.com/package/@alxia/client)
9
+ calls.
10
+
11
+ ```sh
12
+ bun add @alxia/core
13
+ bun add -d typescript
14
+ ```
15
+
16
+ Everything else is a package of its own, to take or leave:
17
+ [`@alxia/zod`](https://www.npmjs.com/package/@alxia/zod),
18
+ [`@alxia/openapi`](https://www.npmjs.com/package/@alxia/openapi),
19
+ [`@alxia/graphql`](https://www.npmjs.com/package/@alxia/graphql),
20
+ [`@alxia/cors`](https://www.npmjs.com/package/@alxia/cors),
21
+ [`@alxia/secure-headers`](https://www.npmjs.com/package/@alxia/secure-headers),
22
+ [`@alxia/rate-limit`](https://www.npmjs.com/package/@alxia/rate-limit),
23
+ [`@alxia/compress`](https://www.npmjs.com/package/@alxia/compress),
24
+ [`@alxia/jwt`](https://www.npmjs.com/package/@alxia/jwt),
25
+ [`@alxia/logger`](https://www.npmjs.com/package/@alxia/logger),
26
+ [`@alxia/env`](https://www.npmjs.com/package/@alxia/env).
27
+
28
+ ## A first app
29
+
30
+ ```ts
31
+ import { alxia } from '@alxia/core';
32
+ import { zq } from '@alxia/zod';
33
+ import { z } from 'zod';
34
+
35
+ const User = z.object({ id: z.number(), name: z.string() });
36
+
37
+ const app = alxia()
38
+ .get(
39
+ '/users/:id',
40
+ {
41
+ params: z.object({ id: zq.int() }),
42
+ response: { 200: User, 404: z.object({ error: z.literal('not_found') }) },
43
+ },
44
+ async ({ params, reply }) => {
45
+ const user = await findUser(params.id); // params.id: number
46
+ return user ? reply(200, user) : reply(404, { error: 'not_found' });
47
+ },
48
+ )
49
+ .post(
50
+ '/users',
51
+ { body: z.object({ name: z.string().min(1) }), response: { 201: User } },
52
+ async ({ body, reply }) => reply(201, await createUser(body.name)),
53
+ );
54
+
55
+ app.listen(3000);
56
+
57
+ export type App = typeof app;
58
+ ```
59
+
60
+ `listen` hands the routes to `Bun.serve`'s own router. `app.fetch` is the
61
+ same app as a fetch handler; `app.request('/users/1')` calls it in process.
62
+
63
+ ## What the types refuse
64
+
65
+ Each of these is a compile error, not a runtime surprise:
66
+
67
+ ```ts
68
+ app.get('/users/:id', { params: z.object({ name: z.string() }) }, ...); // not the path's parameters
69
+ app.get('/users', { quey: z.object({}) }, ...); // a typo
70
+ ({ reply }) => reply(201, user); // a status not declared
71
+ ({ reply }) => reply(200, { id: '1' }); // a body its schema refuses
72
+ ```
73
+
74
+ ## Requests
75
+
76
+ | part | read from | without a schema |
77
+ | --- | --- | --- |
78
+ | `params` | the path, as strings | `{ id: string }`, from the path |
79
+ | `query` | the query string: a key given once is a string, more than once an array | `Record<string, string \| string[]>` |
80
+ | `headers` | the headers, names lowercased | `Record<string, string>` |
81
+ | `cookies` | the `Cookie` header | `Record<string, string>` |
82
+ | `body` | by `content-type`: a parser the app added, JSON, a form, text, or the bytes | `undefined`: read `ctx.request` |
83
+
84
+ A request any schema refuses is answered with a 400 that names every issue,
85
+ whatever part it is in:
86
+
87
+ ```json
88
+ { "error": "validation", "issues": [{ "target": "params", "path": ["id"], "code": "invalid_type", "message": "…" }] }
89
+ ```
90
+
91
+ `ip` is the client's address — the `ip` option reads it behind a proxy —
92
+ and `server` the Bun server, when there is one. `HEAD` runs the `GET` route.
93
+
94
+ `query` declares a `QUERY` route: a safe, idempotent read whose criteria are
95
+ too long or too structured for a query string, so they travel in the body —
96
+ validated like a `POST`'s, a 400 when refused.
97
+
98
+ ```ts
99
+ app.query(
100
+ '/users/search',
101
+ { body: z.object({ name: z.string().min(1) }), response: { 200: z.array(User) } },
102
+ async ({ body, reply }) => reply.ok(await searchUsers(body.name)),
103
+ );
104
+ ```
105
+
106
+ `route(operation, handler)` declares the same route from data —
107
+ `{ method, path, schema? }`, written once and shared, or generated from an
108
+ OpenAPI document — with the same types and the same compile errors. A
109
+ variable holding an operation needs `as const`, to keep its method and path
110
+ literal:
111
+
112
+ ```ts
113
+ const getUser = {
114
+ method: 'GET',
115
+ path: '/users/:id',
116
+ schema: { params: z.object({ id: z.coerce.number().int() }), response: { 200: User } },
117
+ } as const;
118
+
119
+ app.route(getUser, async ({ params, reply }) => reply.ok(await findUser(params.id)));
120
+ ```
121
+
122
+ ## Replies
123
+
124
+ A handler returns `reply(status, body, init?)`. With `response` schemas,
125
+ only a declared status, with a body its schema accepts. Without, any status
126
+ and any body — the client still reads the type of the body. Shortcuts —
127
+ `reply.ok(body)`, `reply.created(body)`, `reply.noContent()`,
128
+ `reply.notFound(body)`, `reply.html(status, html)`, … — are the same
129
+ replies; with schemas, a route has one only for a status it declares.
130
+
131
+ The body sent is the **output** of the schema: an unknown key it strips — a
132
+ password hash — never leaves the server. A reply its schema refuses is a
133
+ 500, never an undeclared shape (`validateResponses: false` skips the check).
134
+
135
+ A string is `text/plain`, a `Blob` — a `Bun.file` — a stream or a buffer
136
+ goes as it is, an async iterable is a stream of server-sent events, anything
137
+ else is JSON. `redirect(location, status?)` needs no schema.
138
+ `set.headers` and `set.cookies` (a `Bun.CookieMap`) apply to every reply.
139
+
140
+ ## Static files
141
+
142
+ Served through the app's pipeline: every hook runs around them — headers,
143
+ compression, telemetry — and the client types them like any route.
144
+
145
+ ```ts
146
+ const app = alxia()
147
+ .static('/assets', './public', {
148
+ cacheControl: (path) =>
149
+ /\.[0-9a-f]{8}\./.test(path) ? 'public, max-age=31536000, immutable' : 'no-cache',
150
+ precompressed: ['br', 'gzip'], // app.js.br, app.js.gz beside app.js
151
+ })
152
+ .file('/favicon.ico', './static/favicon.ico')
153
+ .static('/', './dist', { fallback: 'index.html' }); // a single-page app
154
+ ```
155
+
156
+ `static(path, source, options?)` is a `GET` route at `path/*`. `file(path,
157
+ file, options?)` serves one file. Both answer:
158
+
159
+ - **304** to a client whose copy is current: a weak `ETag` and
160
+ `Last-Modified`;
161
+ - **206** to a `Range` — a video seeking — and **416** to one past the end;
162
+ `If-Range` honored;
163
+ - **404** `{ error: 'not_found' }` to no file, a dotfile, or a path that
164
+ leaves the source — `..`, an encoded slash, a backslash;
165
+ - `HEAD`, as every `GET` route.
166
+
167
+ | option | default | |
168
+ | --- | --- | --- |
169
+ | `index` | `'index.html'` | the file a directory serves: one, a list tried in order, or `false` |
170
+ | `extensions` | none | tried for a path without one: `['html']` serves `/about` from `about.html` |
171
+ | `fallback` | none | served with a 200 for a path that matches no file: a single-page app |
172
+ | `precompressed` | none | `br`, `zstd`, `gzip`: a file stored compressed beside itself, to a client that accepts it |
173
+ | `cacheControl` | `public, max-age=0` | a value, `false`, or one per path |
174
+ | `headers` | none | headers, or `(path, file) => headers` |
175
+ | `types` | Bun's | content types by extension: `{ '.wasm': 'application/wasm' }` |
176
+ | `etag`, `lastModified`, `ranges` | on | |
177
+ | `dotfiles` | `false` | whether `.env` and the like are served |
178
+
179
+ **Any source.** A directory, or a function from a path to a `Blob` — so
180
+ files come from anywhere Bun reads them:
181
+
182
+ ```ts
183
+ const files = new Map([['logo.svg', new File([svg], 'logo.svg', { type: 'image/svg+xml' })]]);
184
+ app.static('/memory', (path) => files.get(path)); // files held in memory
185
+
186
+ app.static('/media', async (path) => { // an S3 bucket
187
+ const file = Bun.s3.file(`media/${path}`);
188
+ return (await file.exists()) ? file : null; // null is a 404
189
+ });
190
+ app.file('/sitemap.xml', async () => new Blob([await sitemap()], { type: 'application/xml' }));
191
+ ```
192
+
193
+ ### Bun's HTML bundles
194
+
195
+ ```ts
196
+ import dashboard from './dashboard/index.html';
197
+
198
+ app.page('/dashboard', dashboard).listen(3000);
199
+ ```
200
+
201
+ `page(path, bundle)` hands Bun's full-stack bundling its route: the page's
202
+ scripts and styles bundled by Bun, hot-reloaded under `development`. It is
203
+ served by `Bun.serve` itself — so through `listen` only, and outside the
204
+ app's hooks.
205
+
206
+ ## Server-sent events
207
+
208
+ ```ts
209
+ import { eventStream } from '@alxia/core';
210
+
211
+ app.get('/ticks', { response: { 200: eventStream(Tick) } }, ({ reply }) =>
212
+ reply(200, (async function* () {
213
+ for (let n = 0; ; n++) { yield { n }; await Bun.sleep(1000); }
214
+ })()),
215
+ );
216
+ ```
217
+
218
+ Each value is checked by the event's schema and sent as one `data:` line of
219
+ JSON; a comment keeps an idle stream open, and the generator is closed when
220
+ the client leaves. The client reads `data` as an `AsyncIterable` of events.
221
+
222
+ ## WebSockets
223
+
224
+ ```ts
225
+ app.ws('/rooms/:room', { message: Chat, send: Chat }, {
226
+ open: (socket) => socket.subscribe(socket.data.params.room),
227
+ message: (socket, chat) => socket.publish(socket.data.params.room, chat),
228
+ });
229
+ ```
230
+
231
+ The upgrade request runs the hooks before the route and is validated as a
232
+ route's — a 401 or a 400 never becomes a socket. Each message is parsed as
233
+ JSON and checked by `message` (a refused one is answered with its issues,
234
+ the socket kept open); each one sent is checked by `send`. `socket.data`
235
+ holds the validated request and what each hook added. Sockets need a
236
+ server: `listen`, or `Bun.serve({ fetch: app.fetch, websocket: app.websocket })`.
237
+
238
+ ## Hooks
239
+
240
+ Route hooks apply to the routes declared **after** them: the chain reads in
241
+ the order the request runs.
242
+
243
+ ```ts
244
+ const app = alxia()
245
+ .decorate({ db }) // ctx.db, everywhere after
246
+ .get('/health', ({ reply }) => reply(200, 'ok')) // not guarded
247
+ .derive(async ({ request, reply }) => {
248
+ const user = await authenticate(request);
249
+ return user ? { user } : reply(401, { error: 'unauthenticated' as const });
250
+ })
251
+ .get('/me', ({ user, reply }) => reply(200, user)); // ctx.user is typed
252
+ ```
253
+
254
+ A reply a hook returns ends the request, and is added to the type of every
255
+ route after it: the client of `/me` reads the 401.
256
+
257
+ `wrap(hook)` is a route hook around the rest: `next()` runs the hooks
258
+ declared after it, validation and the handler, and resolves to the
259
+ response, which the hook returns — or a reply of its own, typed like a
260
+ `derive`'s. An idempotency key, a transaction, a cookie set after the route:
261
+
262
+ ```ts
263
+ .wrap(async ({ request, reply }, next) =>
264
+ busy(request) ? reply(409, { error: 'busy' as const }) : next())
265
+ ```
266
+
267
+ Hooks run before validation: `pathParams` holds the path's parameters as
268
+ they arrived. `onError` turns a thrown
269
+ error into a reply the same way; an `HttpError` is answered as it says, and
270
+ anything else is a 500 that leaks nothing.
271
+
272
+ Global hooks apply to the whole app, wherever they are declared:
273
+
274
+ | hook | |
275
+ | --- | --- |
276
+ | `around(ctx, next)` | around everything else, the first declared outermost: `next()` resolves to the response, and what the hook awaits around it — a span, a transaction — holds for the whole request. `ctx.route` and `ctx.error` say what it reached and how it failed |
277
+ | `onRequest(ctx)` | before routing, every request; a `Response` it returns is sent as it is (a CORS preflight) |
278
+ | `onResponse(response, ctx)` | every response, 404s included; one it returns replaces it (headers, compression) |
279
+ | `onStart(server)`, `onStop()` | with `listen` and `stop` |
280
+ | `parser(type, parse)` | a body parser, tried before the built-in ones |
281
+
282
+ ## Groups
283
+
284
+ ```ts
285
+ app.group('/admin', (admin) =>
286
+ admin.derive(requireAdmin).get('/stats', ...), // the guard applies here only
287
+ );
288
+ ```
289
+
290
+ A group's routes are under its prefix and keep the hooks declared before
291
+ it; the hooks it adds stay inside. `group(build)`, without a prefix, is a
292
+ scope alone.
293
+
294
+ ## Plugins
295
+
296
+ A plugin is an app, or a function.
297
+
298
+ ```ts
299
+ // an app: its routes, its context, its replies — all typed
300
+ const auth = alxia().derive(async ({ request }) => ({ user: await authenticate(request) }));
301
+ const users = alxia({ prefix: '/users' }).get('/:id', ...);
302
+
303
+ const app = alxia({ prefix: '/api' }).use(auth).use(users); // GET /api/users/:id
304
+
305
+ // a function: global hooks, the app's type unchanged
306
+ const poweredBy = (name: string): Plugin => (app) =>
307
+ app.onResponse((response) => { response.headers.set('x-powered-by', name); });
308
+ ```
309
+
310
+ `use(app)` mounts its routes under this app's prefix and behind this app's
311
+ hooks; its route hooks then apply to the routes declared after it, and its
312
+ global hooks become this app's.
313
+
314
+ A plugin that reads what an earlier one added names it with `definePlugin`,
315
+ and an app that does not give it cannot use it:
316
+
317
+ ```ts
318
+ import { alxia, definePlugin } from '@alxia/core';
319
+
320
+ const tenants = new Map<string, { name: string }>();
321
+ const auth = alxia().derive(({ request, reply }) => {
322
+ const tenantId = request.headers.get('x-tenant');
323
+ return tenantId ? { user: { tenantId } } : reply(401, { error: 'unauthenticated' as const });
324
+ });
325
+
326
+ const tenant = definePlugin<{ user: { tenantId: string } }>()((app) =>
327
+ app.derive(({ user }) => ({ tenant: tenants.get(user.tenantId) ?? null })),
328
+ );
329
+
330
+ alxia().use(auth).use(tenant); // ok: auth adds a user, or answers 401
331
+ alxia().use(tenant); // compile error: the plugin reads "user", which this app's context does not give
332
+ ```
333
+
334
+ A tool that reads `app.routes` — a route check, a document — finds a path
335
+ as the core declares and matches it with `joinPath` and `shapeOf`:
336
+
337
+ ```ts
338
+ import { joinPath, shapeOf } from '@alxia/core';
339
+
340
+ joinPath('/api', '/pets/:petId'); // '/api/pets/:petId'
341
+ joinPath('/api', '/'); // '/api'
342
+ shapeOf('/pets/:id') === shapeOf('/pets/:petId'); // true: the router sends them the same requests
343
+ ```
344
+
345
+ [Writing a plugin](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/writing-a-plugin.md)
346
+ covers all three kinds.
347
+
348
+ ## API
349
+
350
+ | export | |
351
+ | --- | --- |
352
+ | `alxia(options?)`, `AlxiaOptions` | a new app: `prefix`, `validateResponses`, `ip` |
353
+ | `Alxia` | `get` `post` `put` `patch` `delete` `options` `head` `query` `route` `ws`, `static` `file` `page`, `decorate` `derive` `wrap` `onError`, `around` `onRequest` `onResponse` `onStart` `onStop` `parser`, `group` `use`, `fetch` `websocket` `request` `listen` `stop`, `routes` `sockets` `server` |
354
+ | `eventStream(schema)`, `EventStreamSchema` | the response schema of a stream of events |
355
+ | `isEventStreamSchema(schema)` | whether a schema is one `eventStream` made |
356
+ | `FileSource`, `StaticOptions`, `FileOptions`, `StaticReply`, `parseRange` | static files |
357
+ | `Precompressed`, `FileNotFoundBody`, `RangeNotSatisfiableBody` | a coding stored beside a file, the bodies of the 404 and 416 |
358
+ | `Reply`, `HttpError`, `ResponseValidationError` | what a handler returns or throws |
359
+ | `ReplyInit` | a reply's options: `headers` |
360
+ | `AnyReply`, `FreeReplyFunction`, `TypedReplyFunction`, `DeclaredReply`, `RedirectFunction` | any reply, `reply` without and with schemas, every reply a route with schemas may return, `redirect` |
361
+ | `FreeShortcuts`, `TypedShortcuts`, `SHORTCUTS`, `Shortcuts` | `reply`'s shortcuts without and with schemas, and the status of each |
362
+ | `Plugin`, `AnyAlxia` | a function plugin, any app |
363
+ | `definePlugin<Requires>()(build)` | an app plugin built on an app whose context has `Requires`; `use` refuses it on an app that does not give them |
364
+ | `Requiring<Requires>`, `ProvidedBy<Ctx, Requires>` | the marker on a `definePlugin` plugin, and the check `use` makes of it |
365
+ | `RequiresOf<Ctx, Callback?>` | what a callback annotated `Ctx` reads beyond `BaseContext` — `{ user: User }` for `BaseContext & { user: User }`, `Empty` for nothing more: the `Requires` of a plugin that infers it from a callback it is given. A callback annotated `any` is refused on every app, with a message naming `Callback` |
366
+ | `ListenOptions` | the options of `listen`: `port`, `hostname`, `development`, `idleTimeout`, `maxRequestBodySize`, `tls` |
367
+ | `RequestHook`, `ResponseHook`, `AroundHook`, `StartHook`, `StopHook`, `BodyParser` | the hooks of `onRequest`, `onResponse`, `around`, `onStart`, `onStop`, and a body parser |
368
+ | `joinPath(prefix, path)` | a path under a prefix, as the app joins them: `joinPath('/api', '/')` is `'/api'`; typed `JoinPath` |
369
+ | `shapeOf(path)` | the path with its parameter names erased, as the router compares them: `shapeOf('/pets/:id') === shapeOf('/pets/:petId')`; throws a `TypeError` for a path no route may be declared at |
370
+ | `withHeaders`, `vary`, `check` | for plugins: edit a response's headers (copied when immutable; an error of the edit leaves the body unread), add to `Vary`, run a schema |
371
+ | `Checked` | what `check` returns: the value, or its issues |
372
+ | `RoutesOf<App>`, `Jsonify<T>` | the route table the client reads, and what a value is on the wire |
373
+ | `RouteTable`, `RouteRecord`, `RouteEntryOf`, `RouteInput`, `RouteOutput`, `Outcome`, `OutcomeOf` | a route as the client knows it: the entry one route adds to `RoutesOf`, what it sends, every outcome it may read |
374
+ | `ContextOf<App>` | what a route declared next on `App` reads: to type a GraphQL schema, a service |
375
+ | `RequestContext`, `BaseContext`, `Context`, `ResponseSettings`, `HandlerResult` | what every hook reads, what a handler reads, what a route sets on its response, what a handler may return |
376
+ | `RouteSchema`, `ResponseSchemas`, `RouteDetail`, `ValidSchema`, `RouteMethod`, `RouteDefinition`, `SocketDefinition` | a route: what it validates, what OpenAPI says of it, the checks its schema's type cannot express, a route method, a route and a socket as the app runs them |
377
+ | `RouteOperation`, `OperationSchema`, `OperationMethod` | a route as data for `route`: `{ method, path, schema? }`, its schema (or `Empty`), and the type of `route` |
378
+ | `SocketSchema`, `SocketContext`, `Socket`, `SocketHandlers`, `SocketSend`, `SocketMessage`, `SocketRecord`, `SocketEntryOf` | sockets: what a socket route validates, what its handlers read, send and receive, the entry one socket adds to `RoutesOf` |
379
+ | `StandardSchemaV1`, `StandardResult`, `StandardIssue`, `InferInput`, `InferOutput` | the Standard Schema types |
380
+ | `ValidationErrorBody`, `InternalErrorBody`, `RoutingErrorBody` | the bodies of the 400, 500, 404, 405 and 426 |
381
+ | `ValidationIssue`, `ValidationTarget` | one issue of a 400, and where the refused value was read from |
382
+ | `RoutePath`, `JoinPath`, `PathParams`, `PathParamName` | paths: an absolute path, a prefix joined to a path, the parameters a path declares |
383
+ | `StatusCode`, `InformationalStatus`, `SuccessStatus`, `RedirectStatus`, `ClientErrorStatus`, `ServerErrorStatus` | every status a route may declare, and each class of them |
384
+ | `Method`, `Empty`, `MaybePromise`, `Simplify` | an HTTP method, no properties, a value or its promise, an object type with its intersections flattened |
385
+
386
+ ## Documentation
387
+
388
+ - [Guide](https://github.com/softistx/alxia/tree/develop/packages/core/docs): a page per area — routes and schemas, replies, hooks, groups and plugins, writing a plugin, static files, server-sent events, WebSockets, serving, and the app's type.
389
+ - [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/core/docs/troubleshooting.md): an error message, and what to do about it.
390
+ - [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/core/docs/roadmap.md): what is coming, and what is not planned.