Docs /backend/setup

Backend Setup & Configuration Guide

Complete guide to setting up and running the Hono + tRPC API server with authentication, middleware, and observability

Table of Contents

  1. Server Setup
  2. Hono Framework Configuration
  3. Middleware & Request Processing
  4. Logging & Observability

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

Reading this with an agent? /docs/backend/setup.md serves the raw markdown.

All docs