Overview of the monorepo structure, design patterns, and architectural decisions.
server-template/
├── apps/ # Deployable applications
│ └── api/ # Main API service
│
├── packages/ # Shared packages
│ ├── config/ # Environment & constants
│ ├── db/ # Database layer
│ └── shared/ # Utilities & middleware
│
├── infra/ # Infrastructure configuration
│ └── monitoring/ # Prometheus, Grafana, Loki
│
├── docs/ # Documentation
├── docker-compose.dev.yml # Local development services
└── turbo.json # Turborepo configuration
┌─────────────────────────────────────────────────────────────┐
│ apps/api │
│ (Main API Service) │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ packages/db │ │ packages/shared │ │ packages/config │
│ (Database) │ │ (Utilities) │ │ (Environment) │
└────────┬────────┘ └────────┬────────┘ └─────────────────┘
│ │ ▲
└────────────────────┴───────────────────┘
│
uses packages/config
| Package | Purpose | Exports |
|---|---|---|
@repo/api |
HTTP API with OAuth authentication | Entry point only |
@repo/db |
Database schemas, services, connection | Schemas, services, types |
@repo/shared |
Utilities, middleware, logging | Logger, JWT, encryption, middleware |
@repo/config |
Environment validation, constants | Env schemas, status codes |
Each feature in apps/api/src/modules/ follows this pattern:
modules/
└── auth/ # Feature module
├── auth.routes.ts # Route registration
├── auth.metrics.ts # Prometheus metrics
├── handlers/ # Request handlers
│ ├── get-oauth.handler.ts
│ ├── get-oauth-callback.handler.ts
│ └── post-refresh-token.handler.ts
├── providers/ # External integrations
│ ├── base.provider.ts # Interface
│ ├── google.provider.ts # Implementation
│ └── index.ts # Factory
└── services/ # Business logic
├── oauth.service.ts
└── token.service.ts
Routes and handlers are colocated with OpenAPI schemas:
// apps/api/src/modules/auth/handlers/get-oauth.handler.ts
import { createRoute, z } from "@hono/zod-openapi";
import type { RouteHandler } from "@hono/zod-openapi";
import { StatusCodes, errorResponseSchemas } from "@repo/config";
// Define route with full OpenAPI schema
export const getOAuthRoute = createRoute({
method: "get",
path: "/oauth/:provider",
tags: ["Auth"],
summary: "Initiate OAuth flow",
request: {
params: z.object({
provider: z.enum(["google"]),
}),
},
responses: {
[StatusCodes.HTTP_302_FOUND]: {
description: "Redirect to OAuth provider",
},
...errorResponseSchemas,
},
});
// Handler with type safety from route definition
export const getOAuthHandler: RouteHandler<typeof getOAuthRoute> = async (c) => {
const { provider } = c.req.valid("params");
// ... implementation
};Database services use namespaces with optional logger:
// packages/db/src/services/users.service.ts
export namespace UsersService {
export async function create(
payload: NewUser,
logger?: LoggerInterface,
options?: { tx?: DBTransaction },
) {
const queryClient = options?.tx || db;
const [user] = await queryClient.insert(usersTable).values(payload).returning();
logger?.audit("User created", {
module: "db",
action: "service:create",
userId: user.id,
});
return user;
}
export async function findByEmail(email: string) {
return db.query.usersTable.findFirst({
where: eq(usersTable.email, email),
});
}
}External integrations use a factory with a common interface:
// Base interface
interface OAuthProvider {
getAuthorizationUrl(state: string): string;
exchangeCode(code: string): Promise<TokenResponse>;
getUserInfo(accessToken: string): Promise<UserInfo>;
}
// Factory
const oauthProviderFactory = {
google: new GoogleProvider(),
// github: new GitHubProvider(),
};
// Usage
const provider = oauthProviderFactory[providerName];
const url = provider.getAuthorizationUrl(state);Centralized app creation with consistent configuration:
// packages/shared/src/create-app.ts
export function createApp() {
const app = new OpenAPIHono({
defaultHook: (result, c) => {
if (!result.success) {
// Consistent validation error handling
return c.json({ errors: result.error.issues }, StatusCodes.HTTP_400_BAD_REQUEST);
}
},
});
// Global middleware
app.use("*", requestLoggerMiddleware());
app.use("*", metricsMiddleware);
app.use("*", globalRateLimiter);
return app;
}
export function createRouter() {
return new OpenAPIHono();
}┌─────────────────────────────────────────────────────────────────────┐
│ HTTP Request │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Global Middleware (in order) │
│ 1. Request Logger → 2. Metrics → 3. Rate Limiter → 4. CORS │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Route Matching │
│ Hono matches path to handler │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Route-Level Middleware │
│ (e.g., authRateLimiter, authentication) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Zod Validation │
│ Validates params, query, body, headers │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Handler │
│ Business logic, calls services, returns response │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Response Sent │
│ Metrics recorded, logs written │
└─────────────────────────────────────────────────────────────────────┘
packages/db/src/schema/
├── index.ts # Re-exports all schemas
└── users/
├── index.ts # Domain exports
├── users.db.ts # Users table
└── sessions.db.ts # Sessions table
// Soft deletes with deletedAt
export const usersTable = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
email: varchar("email", { length: 255 }).notNull().unique(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
deletedAt: timestamp("deleted_at", { withTimezone: true }),
});import { db, type DBTransaction } from "@repo/db";
async function createUserWithSession(payload: NewUser) {
return db.transaction(async (tx) => {
const user = await UsersService.create(payload, { tx });
const session = await SessionService.create({ userId: user.id, provider: "google" }, { tx });
return { user, session };
});
}┌────────┐ ┌─────────┐ ┌──────────────┐ ┌────────────┐
│ Client │────▶│ API │────▶│ OAuth Provider│────▶│ Database │
└────────┘ └─────────┘ └──────────────┘ └────────────┘
│ │ │ │
│ 1. GET /oauth/google │ │
│◀──────────────│ │ │
│ 2. Redirect to Google │ │
│─────────────────────────────────▶│ │
│ 3. User authenticates │ │
│◀─────────────────────────────────│ │
│ 4. Redirect with code │ │
│──────────────▶│ │ │
│ │ 5. Exchange code │ │
│ │─────────────────▶│ │
│ │ 6. Tokens │ │
│ │◀─────────────────│ │
│ │ 7. Create/update user & session │
│ │─────────────────────────────────────▶│
│ 8. JWT tokens │ │
│◀──────────────│ │ │
| Token | Storage | Lifetime | Purpose |
|---|---|---|---|
| Access Token | Client (memory) | 15 min | API authorization |
| Refresh Token | HttpOnly cookie | 7 days | Get new access token |
| OAuth Tokens | Database (encrypted) | Varies | Provider API calls |
- Refresh tokens in DB: AES-256-GCM encryption
- State tokens: Signed JWTs with 10-minute expiry
- Passwords: Never stored (OAuth only)
// packages/shared/src/error-handler.ts
export function errorHandler(err: Error, c: Context) {
if (err instanceof HTTPException) {
return err.getResponse();
}
logger.error("Unhandled error", {
module: "system",
action: "error:unhandled",
error: err,
});
return c.json({ message: "Internal server error" }, StatusCodes.HTTP_500_INTERNAL_SERVER_ERROR);
}All routes include standardized error responses:
import { errorResponseSchemas } from "@repo/config";
export const myRoute = createRoute({
// ... route config
responses: {
[StatusCodes.HTTP_200_OK]: {
/* success */
},
...errorResponseSchemas, // 400, 401, 403, 404, 429, 500
},
});Three tiers available:
import { globalRateLimiter, authRateLimiter, strictRateLimiter } from "@repo/shared";
// Global: 100 requests / 15 minutes (applied to all routes)
app.use("*", globalRateLimiter);
// Auth: 20 requests / 15 minutes (for auth endpoints)
authRouter.use(authRateLimiter);
// Strict: 10 requests / 15 minutes (for sensitive operations)
sensitiveRouter.use(strictRateLimiter);Validated at startup with Zod:
// packages/config/src/env.ts
export const baseEnvSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
LOG_LEVEL: z.enum(["trace", "debug", "info", "warn", "error", "fatal"]).default("info"),
// ... other vars
});
// Fails fast if invalid
export const baseEnv = baseEnvSchema.parse(process.env);Always use the enum:
import { StatusCodes } from "@repo/config";
// ✅ Good
return c.json(data, StatusCodes.HTTP_200_OK);
// ❌ Bad
return c.json(data, 200);mkdir -p apps/api/src/modules/myfeature/{handlers,services}// apps/api/src/modules/myfeature/handlers/get-resource.handler.ts
export const getResourceRoute = createRoute({
/* ... */
});
export const getResourceHandler: RouteHandler<typeof getResourceRoute> = async (c) => {
/* ... */
};// apps/api/src/modules/myfeature/myfeature.routes.ts
import { createRouter } from "@repo/shared";
import { getResourceRoute, getResourceHandler } from "./handlers/get-resource.handler";
export const myFeatureRouter = createRouter().openapi(getResourceRoute, getResourceHandler);// apps/api/src/index.ts
import { myFeatureRouter } from "./modules/myfeature/myfeature.routes";
app.route("/v1/myfeature", myFeatureRouter);# Run all apps in development
bun run dev
# Run specific app
bun run dev --filter=@repo/api
# Generate database migrations
bun run db:generate
# Apply migrations
bun run db:migrate
# Type checking
bun run typecheck
# Linting
bun run lint- AUTHENTICATION.md - OAuth flow details
- DATABASE.md - Schema and service patterns
- MONITORING.md - Metrics and logging
- LOGGING.md - Structured logging guide