serverstruct
Version:
Type safe and modular servers with H3
136 lines (93 loc) • 5.12 kB
Markdown
---
name: serverstruct
description: Use when building an HTTP server with serverstruct — covers app structure, Box dependency injection, handlers and middleware, request context, testing, and where to look for OpenAPI, tracing, and getbox/context details.
---
# Serverstruct
## When to Use
Use this skill when the user wants to:
- create or modify a `serverstruct` HTTP server
- add routes, controllers, handlers, or middleware
- wire services with `getbox`
- test a serverstruct app with mocked dependencies
Open these references only when needed:
- `references/openapi.md` for `serverstruct/openapi`
- `references/otel.md` for tracing with `serverstruct/otel`
- `references/getbox-context.md` for AsyncLocalStorage-style dependency access when the project uses that pattern
## Core Guidance
Prefer `controller()` for the root app and all sub-apps when the app meaningfully uses Box or shared dependencies. `application()` still supports an external box or a `withBox()` scope, but `controller()` is usually less wiring and better matches the common DI-heavy structure.
Define routes inline inside controller setup. Resolve dependencies near the top of the setup closure and close over them in route handlers.
```typescript
import { controller } from "serverstruct";
import { HTTPError, readBody } from "h3";
const usersController = controller((app, box) => {
const deps = box.get({ store: UserStore, logger: Logger });
app.get("/", () => deps.store.getAll());
app.get("/:id", (event) => {
const user = deps.store.find(event.context.params!.id);
if (!user) throw new HTTPError({ status: 404, message: "User not found" });
return user;
});
app.post("/", async (event) => deps.store.add(await readBody(event)));
});
```
Use `handler()`, `eventHandler()`, and OpenAPI `route()` only when a route genuinely needs to be extracted from its controller. Handlers should throw `HTTPError` from `h3` when they need to return an HTTP failure response.
## Middleware, Context, Errors
Use `middleware()` for reusable custom middleware that needs Box or should be extracted from a controller. Use H3 lifecycle helpers like `onRequest()`, `onResponse()`, and `onError()` for lifecycle middleware. When those are defined inline in a controller, they already have access to the controller's Box-scoped dependencies through closure.
Middleware registered with `app.use()` runs globally before the matched handler in the same order it was added.
```typescript
import { middleware } from "serverstruct";
import { HTTPError, onError } from "h3";
const authMiddleware = middleware((event, next, box) => {
const auth = box.get(AuthService);
if (!auth.verify(event.headers.get("authorization"))) {
throw new HTTPError({ status: 401 });
}
});
const usersController = controller((app, box) => {
const logger = box.get(Logger);
app.use(onError((error) => logger.error(error)));
});
```
Use `context()` to pass typed per-request data between middleware and handlers.
```typescript
import { context } from "serverstruct";
const userCtx = context<User>({ onError: "Not authenticated" });
app.use((event) => userCtx.set(event, authenticate(event)));
app.get("/me", (event) => userCtx.get(event));
app.get("/try", (event) => userCtx.lookup(event));
```
Domain and service code should return domain results and leave HTTP concerns to handlers. Handlers should throw `HTTPError` from `h3` when they need to short-circuit the response.
Add a catch-all route at the end of the root app for not-found handling.
```typescript
app.all("**", () => new HTTPError({ status: 404, message: "Not found" }));
```
## Dependency Injection
Register shared state in Box, not module globals. Classes should declare dependencies in the constructor and wire them with `static init`.
Prefer PascalCase names for Box constructors, even when they come from `factory()`, `computed()`, or `constant()`. If a value is something you pass to `box.get()`, name it like a constructor.
```typescript
import { Box } from "getbox";
class UserService {
constructor(
private db: Database,
private logger: Logger,
) {}
static init = Box.init(UserService).get(Database, Logger);
}
```
If the value is a class, make it a valid getbox constructor. Otherwise use `factory()`, `computed()`, or `constant()` as needed.
## Testing
Mock dependencies on a `Box`, resolve the root controller from that box, and test with `app.request()`.
```typescript
import { Box } from "getbox";
const box = new Box();
Box.mock(box, UserStore, { getAll: () => [{ id: "1" }] });
const app = box.get(App);
const res = await app.request("/users");
expect(await res.json()).toEqual([{ id: "1" }]);
```
## Rules of Thumb
- Prefer `controller()` over `application()` once Box-backed composition matters.
- Resolve deps in controller setup before reaching for extracted handlers.
- Use `context()` for request-scoped data.
- Do not mix `app.*` and `router.*` on the same app after `useRouter(app)`.
- Start the OTel SDK before app imports and add `traceMiddleware()` after error handlers when you want exception recording.