Backend Setup & Configuration Guide
Complete guide to setting up and running the Hono + tRPC API server with authentication, middleware, and observability
Table of Contents
Server Setup
The Izri API server is a production-ready backend built with Hono (fast web framework) and tRPC (type-safe RPC).
Overview
Default Port: 4000
Key Features:
- Ultra-fast request handling with Hono
- End-to-end type safety with tRPC
- Built-in OpenTelemetry support
- Better Auth integration with session management
- Comprehensive error handling and logging
Architecture
┌─────────────────────────────────────────────┐
│ Hono HTTP Server │
│ (CORS, Logging, Error Handling) │
└────────────┬────────────────────────────────┘
│
┌────────┴────────┐
│ │
┌───▼────────┐ ┌────▼─────────┐
│ REST APIs │ │ Better Auth │
│ /health │ │ /api/auth/** │
└────────────┘ └──────────────┘
│
┌────────▼────────┐
│ tRPC Server │
│ /trpc/* │
└────────┬────────┘
│
┌────────┴────────────────┐
│ │
┌───▼─────────┐ ┌───────────▼──────────┐
│ Routers │ │ Database (Drizzle) │
│ - auth │ │ - PostgreSQL │
│ - projects │ │ - Migrations │
│ - orgs │ └──────────────────────┘
│ - analysis │
└─────────────┘
Quick Start
Prerequisites:
- Node.js 18+
- PostgreSQL 15+ (or Docker)
- pnpm 8+
Installation:
# Install dependencies from repo root
pnpm install
# Start PostgreSQL (via Docker)
pnpm docker:up
# Postgres exposed on localhost:5433
Run Development Server:
# From repo root
pnpm dev:api
# Or from apps/api directory
cd apps/api
pnpm dev
Verify Server:
# Health check
curl http://localhost:4000/health
# Response: {"ok":true}
# tRPC endpoint
curl http://localhost:4000/trpc/health
Environment Variables
Create a .env file in the repo root:
# Server Configuration
API_PORT=4000
LOG_LEVEL=info # debug | info | warn | error
NODE_ENV=development
# Database
DATABASE_URL=postgresql://postgres:password@localhost:5432/izri
# Authentication
AUTH_URL=http://localhost:4000
BETTER_AUTH_SECRET=your-secret-key-here
# Telemetry
OTEL_ENABLED=false
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
File Structure
apps/api/
├── src/
│ ├── index.ts # Server entry point
│ ├── telemetry.ts # OpenTelemetry setup
│ ├── middleware/
│ │ ├── auth.ts # Authentication middleware
│ │ ├── cors.ts # CORS configuration
│ │ └── error.ts # Error handling middleware
│ ├── routers/
│ │ ├── auth.ts # Auth procedures
│ │ ├── projects.ts # Project procedures
│ │ ├── organizations.ts
│ │ └── index.ts # Combined router
│ └── lib/
│ ├── auth.ts # Auth helpers
│ ├── db.ts # Database connection
│ └── errors.ts # Error definitions
├── package.json
└── tsconfig.json
Hono Framework Configuration
Basic Setup
File: apps/api/src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { createTRPCServer } from './trpc'
const app = new Hono()
// Middleware
app.use('*', cors())
app.use('*', logger())
// Health check
app.get('/health', (c) => c.json({ ok: true }))
// tRPC routes
const trpcServer = createTRPCServer()
app.use('/trpc/*', trpcServer)
// Error handling
app.onError((err, c) => {
console.error(err)
return c.json(
{ error: 'Internal Server Error' },
{ status: 500 }
)
})
export default app
CORS Configuration
import { cors } from 'hono/cors'
app.use('*', cors({
origin: process.env.WEB_URL || 'http://localhost:3000',
credentials: true,
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400,
}))
Request Logging
import { logger } from 'hono/logger'
app.use('*', logger((message) => {
const timestamp = new Date().toISOString()
console.log(`[${timestamp}] ${message}`)
}))
Static File Serving
import { serveStatic } from 'hono/serve-static'
app.use('/public/*', serveStatic({ root: './' }))
Middleware & Request Processing
Authentication Middleware
File: apps/api/src/middleware/auth.ts
import { auth } from '@izri/auth'
import type { Context, Next } from 'hono'
import type { AuthenticatedUser } from '@izri/shared'
declare global {
namespace HonoRequest {
Variables: {
user?: AuthenticatedUser
}
}
}
export async function withAuth(c: Context, next: Next) {
try {
const sessionData = await auth.api.getSession({ headers: c.req.raw.headers })
if (sessionData?.user) {
c.set('user', {
id: sessionData.user.id,
email: sessionData.user.email,
name: sessionData.user.name,
image: sessionData.user.image,
})
}
} catch (error) {
console.error('Auth middleware error:', error)
}
await next()
}
export function requireAuth(c: Context) {
const user = c.get('user')
if (!user) {
throw new Error('Unauthorized')
}
return user
}
Usage:
app.use('/api/protected/*', withAuth)
app.get('/api/protected/profile', (c) => {
const user = requireAuth(c)
return c.json({ user })
})
Custom Middleware
import type { Context, Next } from 'hono'
export async function requestId(c: Context, next: Next) {
const id = crypto.randomUUID()
c.set('requestId', id)
c.header('X-Request-Id', id)
await next()
}
export async function timing(c: Context, next: Next) {
const start = Date.now()
await next()
const duration = Date.now() - start
c.header('X-Response-Time', `${duration}ms`)
}
Error Handling Middleware
import type { Context, Next } from 'hono'
export async function errorHandler(c: Context, next: Next) {
try {
await next()
} catch (error) {
const requestId = c.get('requestId')
console.error(`[${requestId}] Error:`, error)
if (error instanceof ValidationError) {
return c.json(
{ error: error.message, details: error.details },
{ status: 400 }
)
}
if (error instanceof AuthorizationError) {
return c.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
return c.json(
{ error: 'Internal Server Error' },
{ status: 500 }
)
}
}
Database Query Middleware
import type { Context, Next } from 'hono'
import { db } from '@/lib/db'
export async function withDb(c: Context, next: Next) {
c.set('db', db)
await next()
}
export function getDb(c: Context) {
return c.get('db')
}
Usage:
app.use('/api/*', withDb)
app.get('/api/users/:id', async (c) => {
const db = getDb(c)
const user = await db.query.users.findFirst({
where: (users, { eq }) => eq(users.id, c.req.param('id'))
})
return c.json(user)
})
Logging & Observability
Logger Setup
File: apps/api/src/lib/logger.ts
import { pino } from 'pino'
const isProduction = process.env.NODE_ENV === 'production'
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: isProduction ? undefined : {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
},
})
Using Logger
import { logger } from '@/lib/logger'
// Info
logger.info({ userId: '123' }, 'User created')
// Error
logger.error({ error: err }, 'Failed to fetch user')
// Debug
logger.debug({ query }, 'Executing database query')
Request Logging Middleware
import type { Context, Next } from 'hono'
import { logger } from '@/lib/logger'
export async function requestLogger(c: Context, next: Next) {
const start = Date.now()
const method = c.req.method
const path = c.req.path
await next()
const duration = Date.now() - start
const status = c.res.status
logger.info({
method,
path,
status,
duration,
}, `${method} ${path}`)
}
OpenTelemetry Integration
File: apps/api/src/telemetry.ts
import { NodeSDK } from '@opentelemetry/sdk-node'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
if (process.env.OTEL_ENABLED === 'true') {
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
}),
instrumentations: [getNodeAutoInstrumentations()],
})
sdk.start()
process.on('SIGTERM', () => {
sdk.shutdown().catch(console.error)
})
}
Structured Logging Best Practices
// ✓ Good: Structured with context
logger.info({
userId: user.id,
organizationId: org.id,
action: 'project_created',
projectId: project.id,
}, 'Project creation successful')
// ✗ Avoid: Unstructured strings
logger.info(`User ${userId} created project ${projectId}`)
Performance Monitoring
import { performance } from 'perf_hooks'
export async function measurePerformance(name: string, fn: () => Promise<any>) {
const start = performance.now()
try {
const result = await fn()
const duration = performance.now() - start
logger.debug({ duration }, `${name} took ${duration.toFixed(2)}ms`)
return result
} catch (error) {
const duration = performance.now() - start
logger.error({ duration, error }, `${name} failed after ${duration.toFixed(2)}ms`)
throw error
}
}
Related: Authentication & API | Testing | README