@basaltkit/express 1.0.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/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +140 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Machize Contributors
|
|
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
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# @basaltkit/express
|
|
2
|
+
|
|
3
|
+
Basalt adapter for [Express](https://expressjs.com): the same typed routes, enrichers, and guards you'd use in Fastify or Hono, running on an Express server. You need it when you already use Express (or want its huge middleware ecosystem) and want Basalt's validation, per-request context, and standardized errors.
|
|
4
|
+
|
|
5
|
+
## What this module solves
|
|
6
|
+
|
|
7
|
+
[Express](https://expressjs.com) is Node.js's best-known HTTP server — the program that receives **HTTP requests** (messages like "create this project") and returns responses. But Express, by itself, doesn't validate data, doesn't type anything in TypeScript, and every project invents its own error format.
|
|
8
|
+
|
|
9
|
+
This module connects Express to Basalt. **Routes** (address + method, e.g. `POST /echo`) are defined with the `route()` function from `@basaltkit/http`, in a neutral format with [Zod](https://zod.dev) schemas for validation. The adapter converts each Express request into that neutral format, runs the shared pipeline (validation, *enrichers* — functions that enrich the request context, like resolving the tenant — and *guards* — functions that can reject the request, like authentication), and converts errors into JSON responses with a stable format.
|
|
10
|
+
|
|
11
|
+
The strong point: **portability**. A route written for this adapter runs unchanged on `@basaltkit/fastify` and `@basaltkit/hono`. And the neutral edge plugins (security, health, metrics, tracing, OpenAPI) from `@basaltkit/http` work here exactly the same.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @basaltkit/express @basaltkit/core @basaltkit/http express zod
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`express` (version 4.19+ or 5) is a peer dependency — install it yourself. `zod` is required for the route schemas.
|
|
20
|
+
|
|
21
|
+
## Get started in 5 minutes
|
|
22
|
+
|
|
23
|
+
**Step 1** — install the packages (command above).
|
|
24
|
+
|
|
25
|
+
**Step 2** — create a `server.ts` file:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createApp } from '@basaltkit/core'
|
|
29
|
+
import { route } from '@basaltkit/http'
|
|
30
|
+
import { EXPRESS, expressPlugin } from '@basaltkit/express'
|
|
31
|
+
import { z } from 'zod'
|
|
32
|
+
|
|
33
|
+
// 1. Define a route: method, URL, validation, and handler (the function that responds).
|
|
34
|
+
const echo = route({
|
|
35
|
+
method: 'POST',
|
|
36
|
+
url: '/echo',
|
|
37
|
+
body: z.object({ n: z.number() }), // the body must have a number n
|
|
38
|
+
async handler({ body, reply }) {
|
|
39
|
+
// body.n is already validated and typed as number
|
|
40
|
+
return reply.code(201).send({ doubled: body.n * 2 })
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// 2. Create the Basalt app with the Express plugin and start it.
|
|
45
|
+
const app = await createApp({ plugins: [expressPlugin({ routes: [echo] })] }).boot()
|
|
46
|
+
|
|
47
|
+
// 3. Get the Express app from the container and have it listen on a port.
|
|
48
|
+
app.container.get(EXPRESS).listen(3000)
|
|
49
|
+
console.log('Listening on http://localhost:3000')
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Step 3** — run and test:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx tsx server.ts
|
|
56
|
+
curl -X POST http://localhost:3000/echo \
|
|
57
|
+
-H 'content-type: application/json' \
|
|
58
|
+
-d '{"n":21}'
|
|
59
|
+
# → {"doubled":42} (status 201)
|
|
60
|
+
|
|
61
|
+
curl -X POST http://localhost:3000/echo \
|
|
62
|
+
-H 'content-type: application/json' \
|
|
63
|
+
-d '{"n":"nope"}'
|
|
64
|
+
# → 400 {"error":{"code":"HTTP_VALIDATION","part":"body","issues":[...]}}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> The plugin already enables `express.json()` for you — you don't need to configure JSON parsing.
|
|
68
|
+
|
|
69
|
+
## Usage guide
|
|
70
|
+
|
|
71
|
+
### Routes with params, query, and errors
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { HttpError, route } from '@basaltkit/http'
|
|
75
|
+
import { z } from 'zod'
|
|
76
|
+
|
|
77
|
+
const hello = route({
|
|
78
|
+
method: 'GET',
|
|
79
|
+
url: '/hello/:name', // :name is a dynamic URL parameter
|
|
80
|
+
params: z.object({ name: z.string() }),
|
|
81
|
+
async handler({ params }) {
|
|
82
|
+
return { hello: params.name }
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const boom = route({
|
|
87
|
+
method: 'GET',
|
|
88
|
+
url: '/boom',
|
|
89
|
+
async handler() {
|
|
90
|
+
// Intentional error: turns into a 418 response with a stable code
|
|
91
|
+
throw new HttpError(418, 'TEAPOT', "I'm a teapot")
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The error format is identical to the other adapters: `{ error: { code, message, ... } }`. Unexpected errors respond with `500` and `INTERNAL_ERROR`, without exposing internal details.
|
|
97
|
+
|
|
98
|
+
### Enrichers and guards (authentication, tenancy, …)
|
|
99
|
+
|
|
100
|
+
Plugins register these functions in the container's metadata "buckets"; the adapter applies them to every route. Real example (from the package's tests) — a tenancy-style enricher and an auth-style guard:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { createApp, definePlugin, ensureMetadata, tryCtx } from '@basaltkit/core'
|
|
104
|
+
import { HttpError, route, type RequestEnricher, type RouteGuard } from '@basaltkit/http'
|
|
105
|
+
import { EXPRESS, expressPlugin } from '@basaltkit/express'
|
|
106
|
+
import { z } from 'zod'
|
|
107
|
+
|
|
108
|
+
// Enricher: runs before everything else and attaches the tenant to the request context.
|
|
109
|
+
const enricher: RequestEnricher = ({ request, context }) => {
|
|
110
|
+
const tenant = request.headers['x-tenant-id']
|
|
111
|
+
if (typeof tenant === 'string') (context as { tenant?: unknown }).tenant = { id: tenant }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Guard: rejects the request by throwing an error. Reads the route's meta.
|
|
115
|
+
const guard: RouteGuard = ({ route: def, request }) => {
|
|
116
|
+
if (def.meta?.['auth'] && !request.headers['authorization']) {
|
|
117
|
+
throw new HttpError(401, 'AUTH_REQUIRED', 'Authentication required.')
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const myPlugin = definePlugin({
|
|
122
|
+
name: 'my:http',
|
|
123
|
+
register({ container }) {
|
|
124
|
+
const metadata = ensureMetadata(container)
|
|
125
|
+
metadata.add('http:enrichers', enricher)
|
|
126
|
+
metadata.add('http:guards', guard)
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const secure = route({
|
|
131
|
+
method: 'GET',
|
|
132
|
+
url: '/secure',
|
|
133
|
+
meta: { auth: true }, // the guard reads this
|
|
134
|
+
async handler() {
|
|
135
|
+
const tenant = (tryCtx() as { tenant?: { id: string } })?.tenant?.id ?? null
|
|
136
|
+
return { ok: true, tenant }
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const app = await createApp({ plugins: [myPlugin, expressPlugin({ routes: [secure] })] }).boot()
|
|
141
|
+
app.container.get(EXPRESS).listen(3000)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Without `Authorization` → `401 AUTH_REQUIRED`; with the `x-tenant-id: acme` header the handler sees `tenant: 'acme'` via context.
|
|
145
|
+
|
|
146
|
+
### Neutral edge plugins
|
|
147
|
+
|
|
148
|
+
Imported from `@basaltkit/http` and work on Express without changes:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { createApp } from '@basaltkit/core'
|
|
152
|
+
import { healthPlugin, metricsPlugin, route, securityPlugin } from '@basaltkit/http'
|
|
153
|
+
import { EXPRESS, expressPlugin } from '@basaltkit/express'
|
|
154
|
+
|
|
155
|
+
const ping = route({ method: 'GET', url: '/ping', async handler() { return { pong: true } } })
|
|
156
|
+
|
|
157
|
+
const app = await createApp({
|
|
158
|
+
plugins: [
|
|
159
|
+
expressPlugin({ routes: [ping] }),
|
|
160
|
+
securityPlugin({ rateLimit: { limit: 100, windowMs: 60_000 } }), // secure headers + 429 above the limit
|
|
161
|
+
healthPlugin({ checks: { db: () => ({ ok: true }) } }), // GET /livez and /readyz
|
|
162
|
+
metricsPlugin(), // GET /metrics (Prometheus)
|
|
163
|
+
],
|
|
164
|
+
}).boot()
|
|
165
|
+
app.container.get(EXPRESS).listen(3000)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
All the options for these plugins are documented in the [`@basaltkit/http`](../http/README.md) README.
|
|
169
|
+
|
|
170
|
+
### Bring your own Express app
|
|
171
|
+
|
|
172
|
+
If you already have an Express app with your own middleware, pass it to the plugin:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import express from 'express'
|
|
176
|
+
import { expressPlugin } from '@basaltkit/express'
|
|
177
|
+
|
|
178
|
+
const myApp = express()
|
|
179
|
+
// ... your middleware here ...
|
|
180
|
+
expressPlugin({ app: myApp, routes: [] })
|
|
181
|
+
// Note: the plugin still adds express.json().
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Advanced: `registerRoutes()` without the plugin
|
|
185
|
+
|
|
186
|
+
Mount Basalt routes on an Express app directly, without the Basalt lifecycle:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import express from 'express'
|
|
190
|
+
import { route } from '@basaltkit/http'
|
|
191
|
+
import { registerRoutes } from '@basaltkit/express'
|
|
192
|
+
|
|
193
|
+
const app = express()
|
|
194
|
+
app.use(express.json()) // without the plugin, JSON parsing is your responsibility
|
|
195
|
+
const ping = route({ method: 'GET', url: '/ping', async handler() { return { pong: true } } })
|
|
196
|
+
registerRoutes(app, [ping]) // container, enrichers, and guards are optional
|
|
197
|
+
app.listen(3000)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
In this mode each handler already handles its own errors (the wrapper responds with `toErrorResponse`), but there are no edge plugins or route registration for OpenAPI/CLI.
|
|
201
|
+
|
|
202
|
+
## API reference
|
|
203
|
+
|
|
204
|
+
### `expressPlugin(options?)` → Basalt plugin (`basalt:express`)
|
|
205
|
+
|
|
206
|
+
| Option | Type | Required? | Default | Description |
|
|
207
|
+
|---|---|---|---|---|
|
|
208
|
+
| `routes` | `BasaltRoute[]` | No | `[]` | Routes (created with `route()` from `@basaltkit/http`) to mount. |
|
|
209
|
+
| `app` | `Express` | No | new `express()` | Bring your own Express app; either way, `express.json()` is added. |
|
|
210
|
+
|
|
211
|
+
Behavior: registers the Express app under the `EXPRESS` token and an `HttpServerCollector` under the `HTTP_SERVER` token. On the `app:booted` event it mounts everything in the order Express requires: *after-hooks* middleware (metrics/tracing, via `res.on('finish')`) → *pre-hooks* middleware (security/CORS/rate limit; if one of them responds, the route doesn't run) → Basalt routes → extra routes from edge plugins (`/livez`, `/metrics`, …). Publishes the routes in the `'http:routes'` metadata bucket for OpenAPI/CLI/SDK.
|
|
212
|
+
|
|
213
|
+
> Note: unlike `fastifyPlugin`, this plugin has no `shutdown` step — closing the HTTP server returned by `listen()` is your responsibility.
|
|
214
|
+
|
|
215
|
+
### `EXPRESS`
|
|
216
|
+
|
|
217
|
+
Dependency-injection token (`Token<Express>`): `app.container.get(EXPRESS)` returns the Express app so you can call `listen(port)` or add middleware.
|
|
218
|
+
|
|
219
|
+
### `registerRoutes(app, routes, container?, enrichers?, guards?)`
|
|
220
|
+
|
|
221
|
+
| Parameter | Type | Required? | Default | Description |
|
|
222
|
+
|---|---|---|---|---|
|
|
223
|
+
| `app` | `Express` | Yes | — | Express app to mount on. |
|
|
224
|
+
| `routes` | `BasaltRoute[]` | Yes | — | Routes to mount. |
|
|
225
|
+
| `container` | `Container` | No | — | DI container; without it there's no per-request scope or enrichers/guards. |
|
|
226
|
+
| `enrichers` | `RequestEnricher[]` | No | `[]` | Functions that enrich the context before the guards. |
|
|
227
|
+
| `guards` | `RouteGuard[]` | No | `[]` | Functions that can reject the request (by throwing an error). |
|
|
228
|
+
|
|
229
|
+
### What to import from where
|
|
230
|
+
|
|
231
|
+
This package only exports `expressPlugin`, `registerRoutes`, `EXPRESS`, and `ExpressPluginOptions`. Everything else — `route`, `HttpError`, `RequestValidationError`, `securityPlugin`, `healthPlugin`, `metricsPlugin`, `tracingPlugin`, `openapiPlugin`, types like `RequestEnricher`/`RouteGuard` — is imported from **`@basaltkit/http`**.
|
|
232
|
+
|
|
233
|
+
## Common errors and solutions (FAQ)
|
|
234
|
+
|
|
235
|
+
**"`Cannot find module 'express'`."** Express is a peer dependency: `pnpm add express`.
|
|
236
|
+
|
|
237
|
+
**"`body` arrives `undefined` in the handler."** The client has to send the `Content-Type: application/json` header; without it `express.json()` won't parse the body.
|
|
238
|
+
|
|
239
|
+
**"I tried `import { route } from '@basaltkit/express'` and it failed."** The `route()` function isn't exported from this package — import it from `@basaltkit/http` (it's neutral on purpose: the same route runs on Fastify and Hono).
|
|
240
|
+
|
|
241
|
+
**"400 `HTTP_VALIDATION` on a GET with a correct query."** In Express's query everything arrives as text — use `z.coerce.number()` / `z.coerce.boolean()` in your schemas.
|
|
242
|
+
|
|
243
|
+
**"The edge plugins don't respond (`/metrics` gives 404)."** They're mounted on the `app:booted` event: make sure you call `await createApp({...}).boot()` before `listen()` and that `expressPlugin` is in the plugin list (it's the one that registers `HTTP_SERVER`).
|
|
244
|
+
|
|
245
|
+
**"How do I close the server in a test?"** Keep the return value of `listen()`: `const server = app.container.get(EXPRESS).listen(0)` and at the end `server.close()` followed by `await app.shutdown()`.
|
|
246
|
+
|
|
247
|
+
## How it connects to other modules
|
|
248
|
+
|
|
249
|
+
- **`@basaltkit/core`** — `expressPlugin` is a Basalt plugin (`definePlugin`) in the `createApp → boot` lifecycle; it uses the `Container` (tokens `EXPRESS`, `HTTP_SERVER`), the metadata buckets, and the per-request context (`ctx()`/`tryCtx()`), available at any depth of the code.
|
|
250
|
+
- **`@basaltkit/http`** — provides `route()`, the `runRoute()` pipeline (validation, enrichers, guards), `toErrorResponse()`, and the edge plugins. This adapter simply converts Express's `Request`/`Response` into the neutral `HttpRequest`/`HttpReply`.
|
|
251
|
+
- **`@basaltkit/fastify` / `@basaltkit/hono`** — sibling adapters: the same routes, enrichers, guards, and edge plugins run on any of them unchanged; switching frameworks is just switching plugins.
|
|
252
|
+
- **`@basaltkit/auth` / `@basaltkit/tenancy` / `@basaltkit/permissions`** — register guards/enrichers in `'http:guards'`/`'http:enrichers'` and read the routes' `meta` (e.g. `meta: { auth: true }`); this adapter applies them automatically.
|
|
253
|
+
- **`@basaltkit/sdk` and the CLI** — consume the `'http:routes'` bucket (routes + Zod schemas) that this plugin publishes.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as _basaltkit_core from '@basaltkit/core';
|
|
2
|
+
import { Container } from '@basaltkit/core';
|
|
3
|
+
import { BasaltRoute, RequestEnricher, RouteGuard } from '@basaltkit/http';
|
|
4
|
+
import express, { Express } from 'express';
|
|
5
|
+
|
|
6
|
+
declare const EXPRESS: _basaltkit_core.Token<express.Express>;
|
|
7
|
+
/** Mounts Basalt routes on an Express app (usable without the plugin). */
|
|
8
|
+
declare function registerRoutes(app: Express, routes: BasaltRoute[], container?: Container, enrichers?: RequestEnricher[], guards?: RouteGuard[]): void;
|
|
9
|
+
interface ExpressPluginOptions {
|
|
10
|
+
routes?: BasaltRoute[];
|
|
11
|
+
/** Bring your own Express app; otherwise one is created with `express.json()`. */
|
|
12
|
+
app?: Express;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Runs Basalt on Express. The same routes, enrichers, guards and edge plugins
|
|
16
|
+
* you register for Fastify work unchanged — resolve `EXPRESS` for the app to
|
|
17
|
+
* `listen()`.
|
|
18
|
+
*/
|
|
19
|
+
declare function expressPlugin(options?: ExpressPluginOptions): _basaltkit_core.BasaltPlugin<unknown>;
|
|
20
|
+
|
|
21
|
+
export { EXPRESS, type ExpressPluginOptions, expressPlugin, registerRoutes };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createToken, definePlugin, ensureMetadata } from "@basaltkit/core";
|
|
3
|
+
import {
|
|
4
|
+
HttpServerCollector,
|
|
5
|
+
HTTP_SERVER,
|
|
6
|
+
runRoute,
|
|
7
|
+
toErrorResponse
|
|
8
|
+
} from "@basaltkit/http";
|
|
9
|
+
import express from "express";
|
|
10
|
+
var EXPRESS = createToken("express");
|
|
11
|
+
function toNeutralRequest(req) {
|
|
12
|
+
return {
|
|
13
|
+
method: req.method,
|
|
14
|
+
url: req.originalUrl,
|
|
15
|
+
headers: req.headers,
|
|
16
|
+
params: req.params,
|
|
17
|
+
query: req.query,
|
|
18
|
+
body: req.body,
|
|
19
|
+
...req.ip ? { ip: req.ip } : {},
|
|
20
|
+
...req.route?.path ? { routePattern: String(req.route.path) } : {},
|
|
21
|
+
raw: req
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
var ExpressReply = class {
|
|
25
|
+
constructor(res) {
|
|
26
|
+
this.res = res;
|
|
27
|
+
}
|
|
28
|
+
res;
|
|
29
|
+
_status = 200;
|
|
30
|
+
_sent = false;
|
|
31
|
+
get sent() {
|
|
32
|
+
return this._sent;
|
|
33
|
+
}
|
|
34
|
+
get statusCode() {
|
|
35
|
+
return this._status;
|
|
36
|
+
}
|
|
37
|
+
get raw() {
|
|
38
|
+
return this.res;
|
|
39
|
+
}
|
|
40
|
+
code(status) {
|
|
41
|
+
this._status = status;
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
header(name, value) {
|
|
45
|
+
this.res.setHeader(name, value);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
send(payload) {
|
|
49
|
+
this._sent = true;
|
|
50
|
+
this.res.status(this._status);
|
|
51
|
+
if (payload === void 0 || payload === null) this.res.end();
|
|
52
|
+
else if (typeof payload === "string") this.res.send(payload);
|
|
53
|
+
else this.res.json(payload);
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function basaltHandler(definition, container, enrichers, guards) {
|
|
58
|
+
return async (req, res) => {
|
|
59
|
+
const reply = new ExpressReply(res);
|
|
60
|
+
try {
|
|
61
|
+
const result = await runRoute(definition, toNeutralRequest(req), reply, {
|
|
62
|
+
...container ? { container } : {},
|
|
63
|
+
enrichers,
|
|
64
|
+
guards
|
|
65
|
+
});
|
|
66
|
+
if (!reply.sent) reply.send(result);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const { status, body } = toErrorResponse(error);
|
|
69
|
+
if (!res.headersSent) res.status(status).json(body);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function registerRoutes(app, routes, container, enrichers = [], guards = []) {
|
|
74
|
+
const router = app;
|
|
75
|
+
for (const definition of routes) {
|
|
76
|
+
router[definition.method.toLowerCase()](definition.url, basaltHandler(definition, container, enrichers, guards));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function expressPlugin(options = {}) {
|
|
80
|
+
const collector = new HttpServerCollector();
|
|
81
|
+
return definePlugin({
|
|
82
|
+
name: "basalt:express",
|
|
83
|
+
register({ container }) {
|
|
84
|
+
container.singleton(EXPRESS, () => {
|
|
85
|
+
const app = options.app ?? express();
|
|
86
|
+
app.use(express.json());
|
|
87
|
+
return app;
|
|
88
|
+
});
|
|
89
|
+
container.singleton(HTTP_SERVER, () => collector);
|
|
90
|
+
},
|
|
91
|
+
boot({ container, hooks }) {
|
|
92
|
+
const app = container.get(EXPRESS);
|
|
93
|
+
const routes = options.routes ?? [];
|
|
94
|
+
const metadata = ensureMetadata(container);
|
|
95
|
+
const enrichers = metadata.get("http:enrichers");
|
|
96
|
+
const guards = metadata.get("http:guards");
|
|
97
|
+
const router = app;
|
|
98
|
+
hooks.on("app:booted", () => {
|
|
99
|
+
if (collector.afterHooks.length) {
|
|
100
|
+
app.use((req, res, next) => {
|
|
101
|
+
const start = Date.now();
|
|
102
|
+
res.on("finish", () => {
|
|
103
|
+
void collector.runAfter(toNeutralRequest(req), new ExpressReply(res), res.statusCode, Date.now() - start);
|
|
104
|
+
});
|
|
105
|
+
next();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
app.use(async (req, res, next) => {
|
|
109
|
+
const reply = new ExpressReply(res);
|
|
110
|
+
if (await collector.runPre(toNeutralRequest(req), reply)) return;
|
|
111
|
+
next();
|
|
112
|
+
});
|
|
113
|
+
registerRoutes(app, routes, container, enrichers, guards);
|
|
114
|
+
for (const { method, url, handler } of collector.extraRoutes) {
|
|
115
|
+
router[method.toLowerCase()](url, async (req, res) => {
|
|
116
|
+
const reply = new ExpressReply(res);
|
|
117
|
+
const result = await handler({ request: toNeutralRequest(req), reply });
|
|
118
|
+
if (!reply.sent) reply.send(result);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
for (const definition of routes) {
|
|
123
|
+
metadata.add("http:routes", {
|
|
124
|
+
method: definition.method,
|
|
125
|
+
url: definition.url,
|
|
126
|
+
meta: definition.meta ?? {},
|
|
127
|
+
body: definition.body,
|
|
128
|
+
query: definition.query,
|
|
129
|
+
params: definition.params,
|
|
130
|
+
response: definition.response
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
EXPRESS,
|
|
138
|
+
expressPlugin,
|
|
139
|
+
registerRoutes
|
|
140
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@basaltkit/express",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Express adapter for Basalt: run the same typed routes, enrichers and guards on Express that you would on Fastify or Hono.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@basaltkit/core": "^1.0.0",
|
|
18
|
+
"@basaltkit/http": "^1.0.0"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"express": "^4.19.0 || ^5.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/express": "^5.0.0",
|
|
25
|
+
"@types/node": "^22.15.0",
|
|
26
|
+
"express": "^5.1.0",
|
|
27
|
+
"tsup": "^8.4.0",
|
|
28
|
+
"typescript": "^5.8.0",
|
|
29
|
+
"vitest": "^3.1.0",
|
|
30
|
+
"zod": "^3.24.0",
|
|
31
|
+
"@basaltkit/tsconfig": "^0.24.0"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/Zebedeu/basalt.git",
|
|
39
|
+
"directory": "packages/express"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/Zebedeu/basalt/tree/main/packages/express#readme",
|
|
42
|
+
"bugs": "https://github.com/Zebedeu/basalt/issues",
|
|
43
|
+
"keywords": [
|
|
44
|
+
"basalt",
|
|
45
|
+
"typescript",
|
|
46
|
+
"http",
|
|
47
|
+
"express",
|
|
48
|
+
"adapter"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"typecheck": "tsc --noEmit"
|
|
54
|
+
}
|
|
55
|
+
}
|