serverstruct 1.0.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/README.md CHANGED
@@ -2,7 +2,12 @@
2
2
 
3
3
  ⚡️ Typesafe and modular servers with [H3](https://github.com/unjs/h3).
4
4
 
5
- Serverstruct provides simple helpers for building modular h3 applications with dependency injection using [getbox](https://github.com/eriicafes/getbox).
5
+ Serverstruct provides simple helpers for building modular H3 applications with dependency injection using [getbox](https://github.com/eriicafes/getbox).
6
+
7
+ ## Integrations
8
+
9
+ - [OpenAPI](./OPENAPI.md) - Typesafe OpenAPI operations with Zod schema validation.
10
+ - [OpenTelemetry](./OTEL.md) - Distributed tracing middleware for HTTP requests.
6
11
 
7
12
  ## Installation
8
13
 
@@ -13,173 +18,311 @@ npm i serverstruct h3 getbox
13
18
  ## Quick Start
14
19
 
15
20
  ```typescript
16
- import { application } from "serverstruct";
21
+ import { application, serve } from "serverstruct";
17
22
 
18
23
  const app = application((app) => {
19
24
  app.get("/", () => "Hello world!");
20
25
  });
21
26
 
22
- app.serve({ port: 3000 });
27
+ serve(app, { port: 3000 });
23
28
  ```
24
29
 
25
- ## Composing Apps
30
+ ## Application
26
31
 
27
- Create modular h3 apps and mount them together:
32
+ `application()` creates an H3 instance and a Box instance for dependency injection.
33
+ You can pass a Box instance to share dependencies across applications (see [Box Instance](#box-instance)).
28
34
 
29
- ```typescript
30
- import { application } from "serverstruct";
35
+ You can mount other apps using `app.mount()`:
31
36
 
32
- // Create a users module
33
- const { app: usersApp } = application((app) => {
34
- const users: User[] = [];
37
+ ```typescript
38
+ import { H3 } from "h3";
39
+ import { application, serve } from "serverstruct";
35
40
 
36
- app.get("/", () => users);
37
- app.post("/", async (event) => {
38
- const body = await readValidatedBody(event, validateUser);
39
- users.push(body);
40
- return body;
41
- });
41
+ // Create a sub application
42
+ const usersApp = application((app) => {
43
+ app.get("/", () => ["Alice", "Bob"]);
42
44
  });
43
45
 
44
- // Compose in main app
46
+ // Create a regular H3 instance
47
+ const docsApp = new H3().get("/", () => "API Documentation");
48
+
49
+ // Mount in main app
45
50
  const app = application((app) => {
46
- app.get("/ping", () => "pong");
51
+ app.get("/", () => "Hello world!");
47
52
  app.mount("/users", usersApp);
53
+ app.mount("/docs", docsApp);
48
54
  });
49
55
 
50
- app.serve({ port: 3000 });
56
+ serve(app, { port: 3000 });
51
57
  ```
52
58
 
53
- You can also create and return a new H3 instance to customize H3 options:
59
+ When an app is mounted, its middlewares and routes are copied to the main app in place with middlewares scoped to the base path.
60
+
61
+ Both `application()` and `controller()` can return a custom H3 instance:
54
62
 
55
63
  ```typescript
56
64
  import { H3 } from "h3";
57
65
 
58
- const app = application(() => {
59
- const customApp = new H3({
60
- onError: (error) => {
61
- console.error(error);
66
+ const customApp = application(() => {
67
+ const app = new H3({
68
+ onError: (error, event) => {
69
+ console.error("Error:", error);
62
70
  },
63
71
  });
64
- customApp.get("/", () => "Hello from custom app!");
65
- return customApp;
72
+ app.get("/", () => "Hello from custom app!");
73
+ return app;
66
74
  });
67
75
  ```
68
76
 
69
- ## Controllers with Dependency Injection
77
+ ## Controllers
70
78
 
71
- Use `controller()` to create reusable modules with shared dependencies. It integrates with [getbox](https://github.com/eriicafes/getbox) for dependency injection:
79
+ Controllers are apps that are initialized with a parent Box instance, sharing the same dependency container. Use `controller()` to create H3 app constructors:
72
80
 
73
81
  ```typescript
74
82
  import { application, controller } from "serverstruct";
75
83
 
76
- // Define a controller
77
- const usersController = controller((app) => {
78
- const users: User[] = [];
84
+ class UserStore {
85
+ public users: User[] = [];
79
86
 
80
- app.get("/", () => users);
87
+ add(user: User) {
88
+ this.users.push(user);
89
+ return user;
90
+ }
91
+ }
92
+
93
+ // Create a controller
94
+ const usersController = controller((app, box) => {
95
+ const store = box.get(UserStore);
96
+
97
+ app.get("/", () => store.users);
81
98
  app.post("/", async (event) => {
82
- const body = await readValidatedBody(event, validateUser);
83
- users.push(body);
84
- return body;
99
+ const body = await readBody(event);
100
+ return store.add(body);
85
101
  });
86
102
  });
87
103
 
88
104
  // Use it in your main app
89
105
  const app = application((app, box) => {
90
- app.get("/ping", () => "pong");
106
+ const store = box.get(UserStore);
107
+
108
+ app.get("/count", () => ({
109
+ users: store.users.length,
110
+ }));
91
111
  app.mount("/users", box.new(usersController));
92
112
  });
93
113
 
94
- app.serve({ port: 3000 });
114
+ serve(app, { port: 3000 });
95
115
  ```
96
116
 
97
- The `box` parameter is a getbox `Box` instance that manages dependency instances.
117
+ ## Handlers
98
118
 
99
- ## Sharing Dependencies
119
+ Use `handler()` to create H3 handler constructors:
100
120
 
101
- Controllers can share dependencies using the `box` parameter.
121
+ ```typescript
122
+ import { application, handler } from "serverstruct";
102
123
 
103
- Use `box.get(Class)` to retrieve or create a singleton instance:
124
+ // Define a handler
125
+ const getUserHandler = handler((event, box) => {
126
+ const store = box.get(UserStore);
127
+
128
+ const id = event.context.params?.id;
129
+ return store.users.find((user) => user.id === id);
130
+ });
131
+
132
+ // Use it in your app
133
+ const app = application((app, box) => {
134
+ app.get("/users/:id", box.new(getUserHandler));
135
+ });
136
+ ```
137
+
138
+ ### Event Handlers
139
+
140
+ Use `eventHandler()` to create H3 handler constructors with additional options like meta and middleware:
104
141
 
105
142
  ```typescript
106
- import { application, controller } from "serverstruct";
143
+ import { application, eventHandler } from "serverstruct";
144
+
145
+ // Define an event handler with additional options
146
+ const getUserHandler = eventHandler((box) => ({
147
+ handler(event) {
148
+ const store = box.get(UserStore);
149
+
150
+ const id = event.context.params?.id;
151
+ return store.users.find((user) => user.id === id);
152
+ },
153
+ meta: { auth: true },
154
+ middleware: [],
155
+ }));
156
+
157
+ // Use it in your app
158
+ const app = application((app, box) => {
159
+ app.get("/users/:id", box.new(getUserHandler));
160
+ });
161
+ ```
162
+
163
+ ## Middleware
164
+
165
+ Use `middleware()` to create H3 middleware constructors:
107
166
 
108
- // A shared service
109
- class Database {
110
- users: User[] = [];
167
+ ```typescript
168
+ import { application, middleware } from "serverstruct";
111
169
 
112
- getUsers() { return this.users; }
113
- addUser(user: User) { this.users.push(user); }
170
+ class Logger {
171
+ log(message: string) {
172
+ console.log(message);
173
+ }
114
174
  }
115
175
 
116
- // Controller uses box to access the database
117
- const usersController = controller((app, box) => {
118
- const db = box.get(Database);
176
+ // Define a middleware
177
+ const logMiddleware = middleware((event, next, box) => {
178
+ const logger = box.get(Logger);
179
+ logger.log("Request received");
180
+ });
119
181
 
120
- app.get("/", () => db.getUsers());
121
- app.post("/", async (event) => {
122
- const body = await readValidatedBody(event, validateUser);
123
- db.addUser(body);
124
- return body;
182
+ // Use it in your app
183
+ const app = application((app, box) => {
184
+ app.use(box.get(logMiddleware));
185
+ app.get("/", () => "Hello world!");
186
+ });
187
+ ```
188
+
189
+ All middlewares defined with `app.use()` are global and execute before the matched handler in the exact order they are defined.
190
+
191
+ ## Error Handling
192
+
193
+ Error handlers are middleware that catch errors after calling `await next()`.
194
+
195
+ The last error handler defined executes before earlier ones. The error bubbles through each error handler until a response is returned or the default error response is sent.
196
+
197
+ Use H3's `onError` helper to define error handlers:
198
+
199
+ ```typescript
200
+ import { onError } from "h3";
201
+ import { application } from "serverstruct";
202
+
203
+ const app = application((app) => {
204
+ app.use(
205
+ onError((error) => {
206
+ console.log("Error:", error);
207
+ }),
208
+ );
209
+ app.get("/", () => {
210
+ throw new Error("Oops");
125
211
  });
126
212
  });
213
+ ```
214
+
215
+ When the error handler needs access to the Box, wrap it with `middleware()`:
127
216
 
128
- // Another controller can access the same database
129
- const statsController = controller((app, box) => {
130
- const db = box.get(Database);
217
+ ```typescript
218
+ import { onError } from "h3";
219
+ import { application, middleware } from "serverstruct";
131
220
 
132
- app.get("/count", () => ({ count: db.getUsers().length }));
221
+ const errorHandler = middleware((event, next, box) => {
222
+ return onError((error) => {
223
+ console.log("Error:", error);
224
+ });
133
225
  });
134
226
 
135
227
  const app = application((app, box) => {
136
- app.mount("/users", box.new(usersController));
137
- app.mount("/stats", box.new(statsController));
228
+ app.use(box.get(errorHandler));
229
+ app.get("/", () => {
230
+ throw new Error("Oops");
231
+ });
138
232
  });
139
-
140
- await app.serve({ port: 3000 });
141
233
  ```
142
234
 
143
- `box.get(Class)` creates the instance on first call, then caches it.
144
-
145
- ## Middleware
235
+ ### Not Found Routes
146
236
 
147
- Use h3's native middleware with `app.use()`:
237
+ To catch not found routes, define a catch-all handler and return the desired error:
148
238
 
149
239
  ```typescript
150
- const app = application((app) => {
151
- // Global middleware
152
- app.use(() => console.log("Request received"));
240
+ import { H3Error } from "h3";
153
241
 
242
+ const app = application((app) => {
154
243
  app.get("/", () => "Hello world!");
244
+ app.all("**", () => new H3Error({ status: 404, message: "Not found" }));
155
245
  });
156
246
  ```
157
247
 
158
- Controllers can have their own middleware:
248
+ Mounted apps can define their own not found handlers:
159
249
 
160
250
  ```typescript
161
- const usersController = controller((app) => {
162
- // Runs only for routes in this controller
163
- app.use(() => console.log("Users route accessed"));
251
+ const usersApp = application((app) => {
252
+ app.get("/", () => ["Alice", "Bob"]);
253
+ app.all(
254
+ "**",
255
+ () => new H3Error({ status: 404, message: "User route not found" }),
256
+ );
257
+ });
164
258
 
165
- app.get("/", () => [...]);
166
- app.post("/", async () => {...});
259
+ const app = application((app) => {
260
+ app.mount("/users", usersApp);
261
+ app.all("**", () => new H3Error({ status: 404, message: "Not found" }));
167
262
  });
168
263
  ```
169
264
 
170
- ## Custom Box Instance
265
+ ## Box Instance
171
266
 
172
- Pass your own Box instance to `application()` for more control:
267
+ By default, `application()` creates a new Box instance. Pass a Box instance to reuse it:
173
268
 
174
269
  ```typescript
175
270
  import { Box } from "getbox";
271
+ import { application, serve } from "serverstruct";
176
272
 
177
273
  const box = new Box();
178
274
 
179
- // Pre-populate with dependencies
180
- Box.mock(box, Database, new Database());
275
+ const usersApp = application((app, box) => {
276
+ const store = box.get(UserStore);
277
+ app.get("/", () => store.users);
278
+ }, box);
181
279
 
182
280
  const app = application((app, box) => {
183
- app.mount("/users", box.new(usersController));
281
+ const store = box.get(UserStore);
282
+ app.get("/count", () => store.users.length);
283
+ app.mount("/users", usersApp);
184
284
  }, box);
285
+
286
+ serve(app, { port: 3000 });
287
+ ```
288
+
289
+ ## Context
290
+
291
+ `context()` creates a request-scoped, type-safe store for per-request values.
292
+
293
+ ```typescript
294
+ import { application, context } from "serverstruct";
295
+
296
+ interface User {
297
+ id: string;
298
+ name: string;
299
+ }
300
+
301
+ // Create a context store
302
+ const userContext = context<User>();
303
+
304
+ const app = application((app) => {
305
+ // Set context in middleware
306
+ app.use((event) => {
307
+ const user = { id: "123", name: "Alice" };
308
+ userContext.set(event, user);
309
+ });
310
+
311
+ // Access context in handlers
312
+ app.get("/profile", (event) => {
313
+ // Optional access - returns undefined if not set
314
+ const maybeUser = userContext.lookup(event);
315
+
316
+ // Safe access - throws if not set
317
+ const user = userContext.get(event);
318
+
319
+ return { profile: user };
320
+ });
321
+ });
185
322
  ```
323
+
324
+ ### Context Methods
325
+
326
+ - `set(event, value)` - Store a value for the current request
327
+ - `get(event)` - Retrieve the value for the current request (throws if not found)
328
+ - `lookup(event)` - Retrieve the value or `undefined` if not found
package/dist/index.cjs CHANGED
@@ -1,15 +1,15 @@
1
- let h3 = require("h3");
2
1
  let getbox = require("getbox");
2
+ let h3 = require("h3");
3
3
 
4
4
  //#region src/index.ts
5
5
  /**
6
- * Creates an h3 application with dependency injection support.
6
+ * Creates an h3 application.
7
7
  *
8
- * @param fn - Function that configures the app. Receives a fresh H3 instance
9
- * and Box instance. Can add routes to the provided app, or create
10
- * and return a new H3 instance.
8
+ * @param setup - Function that configures the app. Receives a fresh H3 instance
9
+ * and Box instance. Can add routes to the provided app, or create
10
+ * and return a new H3 instance.
11
11
  * @param box - Optional Box instance. If not provided, creates a new one.
12
- * @returns Object with `app` (H3 instance) and `serve` method.
12
+ * @returns Object with `app` (H3 instance), `box` (Box instance), and `serve` method.
13
13
  *
14
14
  * @example
15
15
  * ```typescript
@@ -30,28 +30,33 @@ let getbox = require("getbox");
30
30
  * await app.serve({ port: 3000 });
31
31
  * ```
32
32
  */
33
- function application(fn, box = new getbox.Box()) {
33
+ function application(setup, box = new getbox.Box()) {
34
34
  const defaultApp = new h3.H3();
35
- const app = fn(defaultApp, box) || defaultApp;
35
+ const app = setup(defaultApp, box) || defaultApp;
36
36
  return {
37
37
  app,
38
+ box,
38
39
  serve: (options) => (0, h3.serve)(app, options)
39
40
  };
40
41
  }
41
42
  /**
42
- * Creates a Constructor that produces an h3 app when resolved.
43
- *
44
- * Use `box.new(controller)` to create fresh controller instances.
45
- * Controllers can use `box.get()` to access shared dependencies.
43
+ * Creates an h3 app constructor.
46
44
  *
47
- * @param fn - Function that configures the controller.
48
- * @returns A Constructor that can be resolved via `box.new(controller)`.
45
+ * @param setup - Function that configures the app.
46
+ * @returns A Constructor that produces an h3 app.
49
47
  *
50
48
  * @example
51
49
  * ```typescript
50
+ * import { application, controller } from "serverstruct";
51
+ *
52
+ * class Database {
53
+ * getUsers() { return ["Alice", "Bob"]; }
54
+ * }
55
+ *
52
56
  * // Define a controller
53
57
  * const usersController = controller((app, box) => {
54
- * app.get("/", () => ["Alice", "Bob"]);
58
+ * const db = box.get(Database);
59
+ * app.get("/", () => db.getUsers());
55
60
  * });
56
61
  *
57
62
  * // Use it in your app
@@ -59,24 +64,188 @@ function application(fn, box = new getbox.Box()) {
59
64
  * app.mount("/users", box.new(usersController));
60
65
  * });
61
66
  * ```
67
+ */
68
+ function controller(setup) {
69
+ return (0, getbox.factory)((box) => application(setup, box).app);
70
+ }
71
+ /**
72
+ * Creates a handler constructor.
73
+ *
74
+ * @param setup - Handler function that receives the event and Box instance.
75
+ * @returns A Constructor that produces an h3 handler.
62
76
  *
63
77
  * @example
64
78
  * ```typescript
65
- * // Controller with shared dependencies
66
- * class Database {
67
- * getUsers() { return ["Alice", "Bob"]; }
79
+ * import { application, handler } from "serverstruct";
80
+ *
81
+ * class UserService {
82
+ * getUser(id: string) { return { id, name: "Alice" }; }
68
83
  * }
69
84
  *
70
- * const usersController = controller((app, box) => {
71
- * const db = box.get(Database);
72
- * app.get("/", () => db.getUsers());
85
+ * // Define a handler
86
+ * const getUserHandler = handler((event, box) => {
87
+ * const userService = box.get(UserService);
88
+ * const id = event.context.params?.id;
89
+ * return userService.getUser(id);
90
+ * });
91
+ *
92
+ * // Use it in your app
93
+ * const app = application((app, box) => {
94
+ * app.get("/users/:id", box.get(getUserHandler));
95
+ * });
96
+ * ```
97
+ */
98
+ function handler(setup) {
99
+ return (0, getbox.factory)((box) => (0, h3.defineHandler)((event) => setup(event, box)));
100
+ }
101
+ /**
102
+ * Creates an event handler constructor from a setup function.
103
+ *
104
+ * @param setup - Function that receives Box instance and returns an event handler object.
105
+ * @returns A Constructor that produces an h3 event handler.
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * import { application, eventHandler } from "serverstruct";
110
+ *
111
+ * class UserService {
112
+ * getUser(id: string) { return { id, name: "Alice" }; }
113
+ * }
114
+ *
115
+ * // Define an event handler
116
+ * const getUserHandler = eventHandler((box) => ({
117
+ * handler(event) {
118
+ * const userService = box.get(UserService);
119
+ * const id = event.context.params?.id;
120
+ * return userService.getUser(id);
121
+ * },
122
+ * meta: { auth: true }
123
+ * }));
124
+ *
125
+ * // Use it in your app
126
+ * const app = application((app, box) => {
127
+ * app.get("/users/:id", box.get(getUserHandler));
128
+ * });
129
+ * ```
130
+ */
131
+ function eventHandler(setup) {
132
+ return (0, getbox.factory)((box) => (0, h3.defineHandler)(setup(box)));
133
+ }
134
+ /**
135
+ * Creates a middleware constructor.
136
+ *
137
+ * @param setup - Middleware function that receives the event, next function, and Box instance.
138
+ * @returns A Constructor that produces an h3 middleware.
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * import { application, middleware } from "serverstruct";
143
+ *
144
+ * class AuthService {
145
+ * validateToken(token: string) { return token === "valid"; }
146
+ * }
147
+ *
148
+ * // Define a middleware
149
+ * const authMiddleware = middleware((event, next, box) => {
150
+ * const authService = box.get(AuthService);
151
+ * const token = event.headers.get("authorization");
152
+ * if (!token || !authService.validateToken(token)) {
153
+ * throw new Error("Unauthorized");
154
+ * }
155
+ * });
156
+ *
157
+ * // Use it in your app
158
+ * const app = application((app, box) => {
159
+ * app.use(box.get(authMiddleware));
160
+ * app.get("/", () => "Hello world!");
161
+ * });
162
+ * ```
163
+ */
164
+ function middleware(setup) {
165
+ return (0, getbox.factory)((box) => (0, h3.defineMiddleware)((event, next) => setup(event, next, box)));
166
+ }
167
+ /**
168
+ * A request-scoped context store for associating values with H3 events.
169
+ *
170
+ * Each request gets its own isolated context that is automatically cleaned up
171
+ * when the request completes. Uses a WeakMap internally to ensure values are
172
+ * garbage collected with their events.
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * import { application, Context } from "serverstruct";
177
+ *
178
+ * const userContext = new Context<User>();
179
+ *
180
+ * const app = application((app) => {
181
+ * app.use((event) => {
182
+ * userContext.set(event, { id: "123", name: "Alice" });
183
+ * });
184
+ * app.get("/user", (event) => {
185
+ * const user = userContext.get(event);
186
+ * return user;
187
+ * });
188
+ * });
189
+ * ```
190
+ */
191
+ var Context = class {
192
+ #map = /* @__PURE__ */ new WeakMap();
193
+ /**
194
+ * Sets a value for the given event.
195
+ */
196
+ set(event, value) {
197
+ this.#map.set(event, value);
198
+ }
199
+ /**
200
+ * Gets the value for the given event.
201
+ * @throws Error if no value is set for the event.
202
+ */
203
+ get(event) {
204
+ if (this.#map.has(event)) return this.#map.get(event);
205
+ throw new Error("context not found");
206
+ }
207
+ /**
208
+ * Gets the value for the given event, or undefined if not set.
209
+ */
210
+ lookup(event) {
211
+ return this.#map.get(event);
212
+ }
213
+ };
214
+ /**
215
+ * Creates a request-scoped context store for associating values with H3 events.
216
+ *
217
+ * Each request gets its own isolated context that is automatically cleaned up
218
+ * when the request completes. Uses a WeakMap internally to ensure values are
219
+ * garbage collected with their events.
220
+ *
221
+ * @returns A Context instance.
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * import { application, context } from "serverstruct";
226
+ *
227
+ * const userContext = context<User>();
228
+ *
229
+ * const app = application((app) => {
230
+ * app.use((event) => {
231
+ * userContext.set(event, { id: "123", name: "Alice" });
232
+ * });
233
+ * app.get("/user", (event) => {
234
+ * const user = userContext.get(event);
235
+ * return user;
236
+ * });
73
237
  * });
74
238
  * ```
75
239
  */
76
- function controller(fn) {
77
- return (0, getbox.factory)((box) => application(fn, box).app);
240
+ function context() {
241
+ return new Context();
78
242
  }
79
243
 
80
244
  //#endregion
245
+ exports.Context = Context;
81
246
  exports.application = application;
82
- exports.controller = controller;
247
+ exports.context = context;
248
+ exports.controller = controller;
249
+ exports.eventHandler = eventHandler;
250
+ exports.handler = handler;
251
+ exports.middleware = middleware;