UNPKG

serverstruct

Version:

Type safe and modular servers with H3

329 lines (233 loc) 7.64 kB
# Serverstruct ⚡️ Typesafe and modular servers with [H3](https://github.com/unjs/h3). Serverstruct provides simple helpers for building modular H3 applications with dependency injection using [getbox](https://github.com/eriicafes/getbox). ## Integrations - [OpenAPI](./OPENAPI.md) - Typesafe OpenAPI operations with Zod schema validation. - [OpenTelemetry](./OTEL.md) - Distributed tracing middleware for HTTP requests. ## Installation ```sh npm i serverstruct h3 getbox ``` ## AI Skills Install the serverstruct agent skills with: ```sh npx skills add eriicafes/serverstruct ``` See [skills/README.md](./skills/README.md) for the available skills. ## Quick Start ```typescript import { application, serve } from "serverstruct"; const app = application((app) => { app.get("/", () => "Hello world!"); }); serve(app, { port: 3000 }); ``` ## Application Use `application()` to create H3 apps. ```typescript import { H3 } from "h3"; import { application, serve } from "serverstruct"; class UserStore { public users: User[] = []; add(user: User) { this.users.push(user); return user; } } // Create an application const app = application((app, box) => { const store = box.get(UserStore); app.get("/", () => store.users); }); serve(app, { port: 3000 }); ``` When mounting a sub-app, all routes will be added with base prefix and global middleware will be added as one prefixed middleware. Both `application()` and `controller()` can return a custom H3 instance: ```typescript import { H3 } from "h3"; const customApp = application(() => { const app = new H3({ onError: (error, event) => { console.error("Error:", error); }, }); app.get("/", () => "Hello from custom app!"); return app; }); ``` **Note:** Sub-app options and global hooks are not inherited when mounted consider setting them in the main app directly. ## Controllers Use `controller()` to create H3 app constructors. You can mount apps using `app.mount()`. ```typescript import { application, controller } from "serverstruct"; // Create a controller const UsersController = controller((app, box) => { const store = box.get(UserStore); app.get("/", () => store.users); }); // Use it in your main app const app = application((app, box) => { const store = box.get(UserStore); app.get("/count", () => store.users.length); app.mount("/users", box.get(UsersController)); }); serve(app, { port: 3000 }); ``` ## Handlers Use `handler()` to create H3 handler constructors: ```typescript import { application, handler } from "serverstruct"; // Define a handler const GetUserHandler = handler((event, box) => { const store = box.get(UserStore); const id = event.context.params?.id; return store.users.find((user) => user.id === id); }); // Use it in your app const app = application((app, box) => { app.get("/users/:id", box.get(GetUserHandler)); }); ``` ### Event Handlers Use `eventHandler()` to create H3 handler constructors with additional options like meta and middleware: ```typescript import { application, eventHandler } from "serverstruct"; // Define an event handler with additional options const GetUserHandler = eventHandler((box) => ({ handler(event) { const store = box.get(UserStore); const id = event.context.params?.id; return store.users.find((user) => user.id === id); }, meta: { auth: true }, middleware: [], })); // Use it in your app const app = application((app, box) => { app.get("/users/:id", box.get(GetUserHandler)); }); ``` ## Middleware Use `middleware()` to create H3 middleware constructors: ```typescript import { application, middleware } from "serverstruct"; class Logger { log(message: string) { console.log(message); } } // Define a middleware const LogMiddleware = middleware((event, next, box) => { const logger = box.get(Logger); logger.log("Request received"); }); // Use it in your app const app = application((app, box) => { app.use(box.get(LogMiddleware)); app.get("/", () => "Hello world!"); }); ``` > All middlewares defined with `app.use()` are global and execute before the matched handler in the exact order they are added to the app. ## Error Handling Error handlers are middleware that catch errors thrown by `await next()`. 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. > You can return or throw errors from handlers, but only `HTTPError` will be exposed to the client. All other errors produce a generic 500 response. Use H3's `onError` helper to define error handlers: ```typescript import { onError } from "h3"; import { application } from "serverstruct"; const app = application((app) => { app.use( onError((error) => { console.log("Error:", error); }), ); app.get("/", () => { throw new Error("Oops"); }); }); ``` ### Not Found Routes To catch not found routes, define a catch-all handler and return the desired error: ```typescript import { HTTPError } from "h3"; const app = application((app) => { app.get("/", () => "Hello world!"); app.all("**", () => new HTTPError({ status: 404, message: "Not found" })); }); ``` Mounted apps can define their own not found handlers: ```typescript const usersApp = application((app) => { app.get("/", () => ["Alice", "Bob"]); app.all( "**", () => new HTTPError({ status: 404, message: "User route not found" }), ); }); const app = application((app) => { app.mount("/users", usersApp); app.all("**", () => new HTTPError({ status: 404, message: "Not found" })); }); ``` ## Box Instance By default, `application()` creates a new Box instance. Pass a Box instance to reuse it: ```typescript import { Box } from "getbox"; import { application, serve } from "serverstruct"; const box = new Box(); const app = application((app, box) => { const store = box.get(UserStore); app.get("/count", () => store.users.length); app.mount("/users", usersApp); }, box); serve(app, { port: 3000 }); ``` If you need to pass a custom Box instance to an application it may be better to use a controller instead, which is also easier to test. ```typescript // app.ts import { Box } from "getbox"; import { controller, serve } from "serverstruct"; export const AppController = controller((app) => { const store = box.get(UserStore); app.get("/count", () => store.users.length); app.mount("/users", usersApp); }); const box = new Box(); const app = box.get(AppController); serve(app, { port: 3000 }); ``` ## Context `context()` creates a request-scoped, type-safe store for per-request values. ```typescript import { application, context } from "serverstruct"; interface User { id: string; name: string; } // Create a context store const userContext = context<User>(); const app = application((app) => { // Set context in middleware app.use((event) => { const user = { id: "123", name: "Alice" }; userContext.set(event, user); }); // Access context in handlers app.get("/profile", (event) => { // returns undefined if not set const maybeUser = userContext.lookup(event); // throws if not set const user = userContext.get(event); return { profile: user }; }); }); ``` ### Context Options - `onError` - Custom error message thrown by `get()` when no value is set. ### Context Methods - `set(event, value)` - Store a value for the current request - `get(event)` - Retrieve the value for the current request (throws if not found) - `lookup(event)` - Retrieve the value or `undefined` if not found