serverstruct
Version:
Type safe and modular servers with H3
329 lines (233 loc) • 7.64 kB
Markdown
# 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