serverstruct 1.1.0 → 1.2.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/.claude/settings.local.json +8 -0
- package/OPENAPI.md +286 -0
- package/OTEL.md +208 -0
- package/README.md +175 -121
- package/dist/openapi.cjs +242 -0
- package/dist/openapi.d.cts +241 -0
- package/dist/openapi.d.mts +241 -0
- package/dist/openapi.mjs +231 -0
- package/dist/openapi.scalar.cjs +13 -0
- package/dist/openapi.scalar.d.cts +8 -0
- package/dist/openapi.scalar.d.mts +8 -0
- package/dist/openapi.scalar.mjs +13 -0
- package/package.json +40 -4
- package/tsdown.config.mts +11 -0
package/OPENAPI.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Serverstruct OpenAPI
|
|
2
|
+
|
|
3
|
+
OpenAPI integration for [serverstruct](https://github.com/eriicafes/serverstruct) with [zod-openapi](https://github.com/samchungy/zod-openapi).
|
|
4
|
+
|
|
5
|
+
Define OpenAPI operations alongside your route handlers using Zod schemas. Request parameters and body are validated at runtime, and responses are fully typed.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm i zod zod-openapi
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { application, serve } from "serverstruct";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import {
|
|
19
|
+
createDocument,
|
|
20
|
+
createRouter,
|
|
21
|
+
jsonRequest,
|
|
22
|
+
jsonResponse,
|
|
23
|
+
OpenApiPaths,
|
|
24
|
+
} from "serverstruct/openapi";
|
|
25
|
+
|
|
26
|
+
const app = application((app, box) => {
|
|
27
|
+
const router = createRouter(app, box.get(OpenApiPaths));
|
|
28
|
+
|
|
29
|
+
router.post(
|
|
30
|
+
"/posts",
|
|
31
|
+
{
|
|
32
|
+
operationId: "createPost",
|
|
33
|
+
requestBody: jsonRequest(
|
|
34
|
+
z.object({ title: z.string() }).meta({ description: "New post input" }),
|
|
35
|
+
),
|
|
36
|
+
responses: {
|
|
37
|
+
201: jsonResponse(
|
|
38
|
+
z.object({ id: z.string() }).meta({ description: "Created post" }),
|
|
39
|
+
{ description: "Post created" },
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
async (event, ctx) => {
|
|
44
|
+
const body = await ctx.body(event);
|
|
45
|
+
// body is typed as { title: string }
|
|
46
|
+
|
|
47
|
+
return ctx.reply(event, 201, { id: "1" });
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Serve the OpenAPI document
|
|
52
|
+
const { paths } = box.get(OpenApiPaths);
|
|
53
|
+
app.get("/docs", () =>
|
|
54
|
+
createDocument({
|
|
55
|
+
openapi: "3.1.0",
|
|
56
|
+
info: { title: "My API", version: "1.0.0" },
|
|
57
|
+
paths,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
serve(app, { port: 3000 });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## OpenApiPaths
|
|
66
|
+
|
|
67
|
+
`OpenApiPaths` collects operation definitions for document generation. Register operations by HTTP method and use the returned `RouterContext` for typed request handling.
|
|
68
|
+
|
|
69
|
+
Since `OpenApiPaths` is a class it can be used as a getbox singleton:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const app = application((app, box) => {
|
|
73
|
+
const paths = box.get(OpenApiPaths);
|
|
74
|
+
|
|
75
|
+
const getUser = paths.get("/users/{id}", {
|
|
76
|
+
operationId: "getUser",
|
|
77
|
+
requestParams: {
|
|
78
|
+
path: z
|
|
79
|
+
.object({ id: z.string() })
|
|
80
|
+
.meta({ description: "User path parameters" }),
|
|
81
|
+
},
|
|
82
|
+
responses: {
|
|
83
|
+
200: {
|
|
84
|
+
description: "User found",
|
|
85
|
+
content: { "application/json": { schema: userSchema } },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
app.get("/users/:id", async (event) => {
|
|
91
|
+
const params = await getValidatedRouterParams(
|
|
92
|
+
event,
|
|
93
|
+
getUser.schemas.params,
|
|
94
|
+
);
|
|
95
|
+
const query = await getValidatedQuery(event, getUser.schemas.query);
|
|
96
|
+
const body = await readValidatedBody(event, getUser.schemas.body);
|
|
97
|
+
// getUser.schemas.headers and getUser.schemas.cookies are also available
|
|
98
|
+
return getUser.reply(event, 200, { id: params.id, name: "Alice" });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Methods
|
|
104
|
+
|
|
105
|
+
- `get(path, operation)` — Register a GET operation
|
|
106
|
+
- `post(path, operation)` — Register a POST operation
|
|
107
|
+
- `put(path, operation)` — Register a PUT operation
|
|
108
|
+
- `delete(path, operation)` — Register a DELETE operation
|
|
109
|
+
- `patch(path, operation)` — Register a PATCH operation
|
|
110
|
+
- `all(path, operation)` — Register for all standard methods
|
|
111
|
+
- `on(methods, path, operation)` — Register for specific methods
|
|
112
|
+
|
|
113
|
+
All methods return a `RouterContext`.
|
|
114
|
+
|
|
115
|
+
## OpenApiRouter
|
|
116
|
+
|
|
117
|
+
`OpenApiRouter` combines OpenAPI path registration with H3 route registration. It converts H3 path syntax (`:param`) to OpenAPI format (`{param}`) automatically. The handler function receives the `RouterContext` as its second argument.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const app = application((app, box) => {
|
|
121
|
+
const router = createRouter(app, box.get(OpenApiPaths));
|
|
122
|
+
|
|
123
|
+
router.get(
|
|
124
|
+
"/users/:id",
|
|
125
|
+
{
|
|
126
|
+
operationId: "getUser",
|
|
127
|
+
requestParams: {
|
|
128
|
+
path: z
|
|
129
|
+
.object({ id: z.string() })
|
|
130
|
+
.meta({ description: "User path parameters" }),
|
|
131
|
+
},
|
|
132
|
+
responses: {
|
|
133
|
+
200: jsonResponse(userSchema, {
|
|
134
|
+
description: "User found",
|
|
135
|
+
headers: z
|
|
136
|
+
.object({ "x-request-id": z.string() })
|
|
137
|
+
.meta({ description: "Response headers" }),
|
|
138
|
+
}),
|
|
139
|
+
404: jsonResponse(errorSchema, { description: "Not found" }),
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
async (event, ctx) => {
|
|
143
|
+
const params = await ctx.params(event);
|
|
144
|
+
const user = findUser(params.id);
|
|
145
|
+
|
|
146
|
+
if (!user) {
|
|
147
|
+
return ctx.reply(event, 404, { error: "not found" });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return ctx.reply(event, 200, user, {
|
|
151
|
+
"x-request-id": crypto.randomUUID(),
|
|
152
|
+
});
|
|
153
|
+
// data and headers are typed per status code
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Methods mirror `OpenApiPaths` but take an additional `handler` and optional `opts` parameter. All methods return `this` for chaining:
|
|
160
|
+
|
|
161
|
+
- `get(path, operation, handler, opts?)`
|
|
162
|
+
- `post(path, operation, handler, opts?)`
|
|
163
|
+
- `put(path, operation, handler, opts?)`
|
|
164
|
+
- `delete(path, operation, handler, opts?)`
|
|
165
|
+
- `patch(path, operation, handler, opts?)`
|
|
166
|
+
- `all(path, operation, handler, opts?)`
|
|
167
|
+
- `on(methods, path, operation, handler, opts?)`
|
|
168
|
+
|
|
169
|
+
## Path Conversion
|
|
170
|
+
|
|
171
|
+
`OpenApiRouter` automatically converts H3 path syntax to OpenAPI format:
|
|
172
|
+
|
|
173
|
+
| H3 | OpenAPI |
|
|
174
|
+
| -------- | ---------- |
|
|
175
|
+
| `/:name` | `/{name}` |
|
|
176
|
+
| `/*` | `/{param}` |
|
|
177
|
+
| `/**` | `/{path}` |
|
|
178
|
+
|
|
179
|
+
When using `OpenApiPaths` directly, paths should be written in OpenAPI format (`/users/{id}`).
|
|
180
|
+
|
|
181
|
+
## Helpers
|
|
182
|
+
|
|
183
|
+
### jsonRequest
|
|
184
|
+
|
|
185
|
+
Build a `requestBody` object with `application/json` content:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
{
|
|
189
|
+
operationId: "createPost",
|
|
190
|
+
requestBody: jsonRequest(
|
|
191
|
+
z.object({ title: z.string() }).meta({ description: "New post input" }),
|
|
192
|
+
),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// With additional options
|
|
196
|
+
{
|
|
197
|
+
operationId: "createPost",
|
|
198
|
+
requestBody: jsonRequest(
|
|
199
|
+
z.object({ title: z.string() }).meta({ description: "New post input" }),
|
|
200
|
+
{ description: "Create a post", content: { example: { title: "Hello" } } },
|
|
201
|
+
),
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### jsonResponse
|
|
206
|
+
|
|
207
|
+
Build a response object with `application/json` content and optional headers:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
{
|
|
211
|
+
operationId: "getUser",
|
|
212
|
+
responses: {
|
|
213
|
+
200: jsonResponse(userSchema, { description: "User found" }),
|
|
214
|
+
404: jsonResponse(errorSchema, { description: "Not found" }),
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// With headers
|
|
219
|
+
{
|
|
220
|
+
operationId: "getUser",
|
|
221
|
+
responses: {
|
|
222
|
+
200: jsonResponse(userSchema, {
|
|
223
|
+
description: "User found",
|
|
224
|
+
headers: z.object({ "x-request-id": z.string() }).meta({ description: "Response headers" }),
|
|
225
|
+
}),
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### metadata
|
|
231
|
+
|
|
232
|
+
Zod's `.meta()` should accept `ZodOpenApiMetadata` directly, but if type inference is not working correctly you can use the `metadata` helper to ensure the correct type:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
const userSchema = z
|
|
236
|
+
.object({
|
|
237
|
+
id: z.string(),
|
|
238
|
+
name: z.string(),
|
|
239
|
+
})
|
|
240
|
+
.meta(
|
|
241
|
+
metadata({
|
|
242
|
+
description: "A user object",
|
|
243
|
+
example: { id: "1", name: "Alice" },
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Generating the Document
|
|
249
|
+
|
|
250
|
+
Use `createDocument` (re-exported from `zod-openapi`) with the accumulated paths:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
const app = application((app, box) => {
|
|
254
|
+
const paths = box.get(OpenApiPaths);
|
|
255
|
+
|
|
256
|
+
// ... register routes ...
|
|
257
|
+
|
|
258
|
+
app.get("/docs", () =>
|
|
259
|
+
createDocument({
|
|
260
|
+
openapi: "3.1.0",
|
|
261
|
+
info: { title: "My API", version: "1.0.0" },
|
|
262
|
+
paths: paths.paths,
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Scalar API Reference
|
|
269
|
+
|
|
270
|
+
Serve an interactive API documentation UI powered by [Scalar](https://github.com/scalar/scalar).
|
|
271
|
+
|
|
272
|
+
```sh
|
|
273
|
+
npm i @scalar/core
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { apiReference } from "serverstruct/openapi/scalar";
|
|
278
|
+
|
|
279
|
+
app.get("/reference", () =>
|
|
280
|
+
apiReference({
|
|
281
|
+
url: "http://localhost:5000/docs",
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The `url` should point to the endpoint serving your OpenAPI document (see [Generating the Document](#generating-the-document) below).
|
package/OTEL.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Serverstruct OpenTelemetry
|
|
2
|
+
|
|
3
|
+
OpenTelemetry distributed tracing integration for [serverstruct](https://github.com/eriicafes/serverstruct).
|
|
4
|
+
|
|
5
|
+
Automatically instrument HTTP requests with OpenTelemetry spans, capturing semantic convention attributes and enabling trace context propagation across microservices.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm i @opentelemetry/api @opentelemetry/semantic-conventions @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/exporter-trace-otlp-http
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { application, serve } from "serverstruct";
|
|
17
|
+
import { traceMiddleware } from "serverstruct/otel";
|
|
18
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
19
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
20
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
21
|
+
import {
|
|
22
|
+
ATTR_SERVICE_NAME,
|
|
23
|
+
ATTR_SERVICE_VERSION,
|
|
24
|
+
} from "@opentelemetry/semantic-conventions";
|
|
25
|
+
|
|
26
|
+
// Initialize and start OpenTelemetry SDK
|
|
27
|
+
const sdk = new NodeSDK({
|
|
28
|
+
resource: resourceFromAttributes({
|
|
29
|
+
[ATTR_SERVICE_NAME]: "my-api",
|
|
30
|
+
[ATTR_SERVICE_VERSION]: "1.0.0",
|
|
31
|
+
}),
|
|
32
|
+
traceExporter: new OTLPTraceExporter({
|
|
33
|
+
url: "http://localhost:4318/v1/traces",
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
sdk.start();
|
|
37
|
+
|
|
38
|
+
const app = application((app) => {
|
|
39
|
+
// Add tracing middleware
|
|
40
|
+
app.use(traceMiddleware());
|
|
41
|
+
|
|
42
|
+
app.get("/users/:id", async (event) => {
|
|
43
|
+
// Spans are automatically created for each request
|
|
44
|
+
return { id: event.context.params.id, name: "Alice" };
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const server = serve(app);
|
|
49
|
+
|
|
50
|
+
// Gracefully shutdown on exit
|
|
51
|
+
process.on("SIGTERM", async () => {
|
|
52
|
+
await server.close();
|
|
53
|
+
await sdk.shutdown();
|
|
54
|
+
process.exit(0);
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Features
|
|
59
|
+
|
|
60
|
+
The middleware automatically captures the following [OpenTelemetry semantic convention](https://opentelemetry.io/docs/specs/semconv/http/http-spans/) attributes:
|
|
61
|
+
|
|
62
|
+
- `http.request.method` - HTTP method
|
|
63
|
+
- `url.full` - Full request URL
|
|
64
|
+
- `url.path` - URL path
|
|
65
|
+
- `url.query` - Query string (if present)
|
|
66
|
+
- `url.scheme` - URL scheme (http/https)
|
|
67
|
+
- `server.address` - Server host
|
|
68
|
+
- `user_agent.original` - User agent header (if present)
|
|
69
|
+
- `http.response.status_code` - Response status code
|
|
70
|
+
|
|
71
|
+
### Span Status Mapping
|
|
72
|
+
|
|
73
|
+
Status codes are automatically mapped to span statuses:
|
|
74
|
+
|
|
75
|
+
- 1xx-4xx: `SpanStatusCode.OK`
|
|
76
|
+
- 5xx: `SpanStatusCode.ERROR`
|
|
77
|
+
|
|
78
|
+
### Exception Recording
|
|
79
|
+
|
|
80
|
+
Exceptions thrown in route handlers are automatically recorded with full stack traces and set the span status to ERROR. The middleware rethrows errors after recording them, so they will still propagate to error handlers.
|
|
81
|
+
|
|
82
|
+
**Middleware Placement**:
|
|
83
|
+
|
|
84
|
+
- Place the tracing middleware **after** error handlers to record exceptions - the trace middleware will catch errors first, record them, then rethrow for error handlers.
|
|
85
|
+
- Place the tracing middleware **before** error handlers to skip exception recording - error handlers will catch errors before they reach the trace middleware.
|
|
86
|
+
- In all cases, span status is still set based on the HTTP response status code (1xx-4xx = OK, 5xx = ERROR).
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
### Custom Span Names
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
app.use(
|
|
94
|
+
traceMiddleware({
|
|
95
|
+
spanName: (event) => `${event.req.method} ${event.path}`,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Custom Span Attributes
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
app.use(
|
|
104
|
+
traceMiddleware({
|
|
105
|
+
spanAttributes: (event) => ({
|
|
106
|
+
"service.name": "my-api",
|
|
107
|
+
"deployment.environment": process.env.NODE_ENV,
|
|
108
|
+
"request.id": event.req.headers.get("x-request-id"),
|
|
109
|
+
}),
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Capture Request/Response Headers
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
app.use(
|
|
118
|
+
traceMiddleware({
|
|
119
|
+
headers: {
|
|
120
|
+
request: ["authorization", "x-api-key"],
|
|
121
|
+
response: ["x-request-id", "x-rate-limit"],
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Headers are captured as:
|
|
128
|
+
|
|
129
|
+
- `http.request.header.<name>` for request headers
|
|
130
|
+
- `http.response.header.<name>` for response headers
|
|
131
|
+
|
|
132
|
+
### Custom Tracer
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { trace } from "@opentelemetry/api";
|
|
136
|
+
|
|
137
|
+
const tracer = trace.getTracer("my-service", "1.0.0");
|
|
138
|
+
|
|
139
|
+
app.use(traceMiddleware({ tracer }));
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Trace Context Propagation
|
|
143
|
+
|
|
144
|
+
Enable distributed tracing across microservices by propagating trace context through HTTP headers.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
app.use(
|
|
148
|
+
traceMiddleware({
|
|
149
|
+
propagation: {
|
|
150
|
+
// Extract trace context from incoming requests (default: true)
|
|
151
|
+
request: true,
|
|
152
|
+
|
|
153
|
+
// Inject trace context into outgoing responses (default: false)
|
|
154
|
+
response: true,
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Custom Propagator
|
|
161
|
+
|
|
162
|
+
Use a custom propagator for trace context extraction/injection:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
166
|
+
|
|
167
|
+
app.use(
|
|
168
|
+
traceMiddleware({
|
|
169
|
+
propagation: {
|
|
170
|
+
propagator: new W3CTraceContextPropagator(),
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Creating Child Spans
|
|
177
|
+
|
|
178
|
+
Create child spans for operations like database queries or external API calls:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { trace } from "@opentelemetry/api";
|
|
182
|
+
|
|
183
|
+
const tracer = trace.getTracer("my-service");
|
|
184
|
+
|
|
185
|
+
app.get("/users/:id", async (event) => {
|
|
186
|
+
const userId = event.context.params.id;
|
|
187
|
+
|
|
188
|
+
// Create a child span for database operation
|
|
189
|
+
return await tracer.startActiveSpan("db.query", async (span) => {
|
|
190
|
+
try {
|
|
191
|
+
const user = await db.getUser(userId);
|
|
192
|
+
span.setAttributes({
|
|
193
|
+
"db.operation": "SELECT",
|
|
194
|
+
"db.table": "users",
|
|
195
|
+
});
|
|
196
|
+
return user;
|
|
197
|
+
} finally {
|
|
198
|
+
span.end();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Learn More
|
|
205
|
+
|
|
206
|
+
- [OpenTelemetry Documentation](https://opentelemetry.io/docs/)
|
|
207
|
+
- [OpenTelemetry JavaScript SDK](https://github.com/open-telemetry/opentelemetry-js)
|
|
208
|
+
- [Semantic Conventions for HTTP](https://opentelemetry.io/docs/specs/semconv/http/http-spans/)
|