Backend Documentation
Hono + tRPC API service
๐ Overview
The Izri backend is a modern, type-safe API server built with Hono (fast web framework) and tRPC (type-safe RPC), using Drizzle ORM for database access. This provides end-to-end type safety from database to frontend with excellent performance.
๐ฏ Technology Stack
- Framework: Hono (Fast, lightweight web server)
- API Layer: tRPC (Type-safe Remote Procedure Calls)
- Database ORM: Drizzle ORM (TypeScript-first ORM)
- Database: PostgreSQL 15
- Cache: Redis 7
- Validation: Zod schemas
- Logging: Pino (structured logging)
- Observability: OpenTelemetry (optional)
- Runtime: Node.js 18+ with tsx for development
๐ Documentation Pages
API Server Setup
Complete API server configuration guide
- Server architecture overview
- Quick start and installation
- Environment configuration
- Hono + tRPC integration
- OpenTelemetry setup
- Docker deployment
- Testing and debugging
tRPC Server
tRPC implementation details
- Router structure
- Procedure types (query vs mutation)
- Context creation
- Middleware implementation
- Input validation
- Type inference
- Client integration
Authentication
Backend authentication strategies
- Authentication architecture
- Session management
- API tokens
- OAuth integration
- Authorization patterns
- Security best practices
Database Schema
Complete database reference
- Full schema documentation
- Table relationships
- Indexes and constraints
- Enums and types
- Query patterns
- Performance optimization
Migrations
Database migration workflow
- Creating migrations
- Applying migrations
- Rolling back changes
- Migration best practices
- Schema versioning
- Production migration strategy
Error Handling
Error management patterns
- Error types and codes
- Error transformation
- Client error handling
- Logging errors
- User-facing errors
- Debugging strategies
Logging
Structured logging and observability
- Pino configuration
- Log levels and structure
- Request logging
- Error logging
- Performance logging
- OpenTelemetry integration
- Log aggregation
๐๏ธ API Structure
apps/api/
โโโ src/
โโโ index.ts # Hono server entry point
packages/trpc/
โโโ src/
โ โโโ context.ts # tRPC context (auth, db)
โ โโโ routers/
โ โ โโโ index.ts # All tRPC procedures
โ โโโ index.ts # Exports
โโโ package.json
packages/database/
โโโ src/
โ โโโ schema.ts # Drizzle schema definitions
โ โโโ client.ts # Database client
โ โโโ seed.ts # Seed script
โ โโโ index.ts # Exports
โโโ migrations/ # SQL migration files
โโโ drizzle.config.ts # Drizzle configuration
๐ Quick Start
Development
# Start API server
pnpm dev:api
# Or from root
pnpm dev
# Access at http://localhost:4000
Testing Endpoints
# Health check (REST)
curl http://localhost:4000/health
# Health check (tRPC)
curl -X POST http://localhost:4000/trpc/health
# Get projects (tRPC)
curl -X POST http://localhost:4000/trpc/projects.getProjects \
-H "Content-Type: application/json" \
-d '{"userEmail":"test@izri.com"}'
๐ก API Endpoints
REST Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /health |
Health check |
| POST | /trpc/* |
tRPC endpoint |
tRPC Procedures
Queries (Read Operations)
| Procedure | Input | Output | Description |
|---|---|---|---|
health |
- | { ok: boolean } |
API health check |
projects.getProjects |
{ userEmail } |
Project[] |
Get user's projects |
projects.getProject |
{ projectId, userEmail } |
Project |
Get single project |
organizations.list |
{ userEmail } |
Organization[] |
List user's organizations |
auth.getSession |
- | SessionInfo |
Get current session |
Mutations (Write Operations)
| Procedure | Input | Output | Description |
|---|---|---|---|
projects.createProject |
CreateProjectInput |
Project |
Create new project |
projects.updateProject |
UpdateProjectInput |
Project |
Update project |
projects.deleteProject |
{ projectId, userEmail } |
{ message } |
Delete project |
organizations.create |
CreateOrganizationInput |
Organization |
Create organization |
analysis.analyzeRepository |
AnalyzeRepositoryInput |
AnalysisResult |
Analyze repository |
๐๏ธ Database Schema
Core Tables
- users: User accounts and profiles
- organizations: Teams and workspaces
- projects: Repository projects
- test_runs: Test execution records
- project_settings: Project configuration
- organization_members: Team memberships
- api_keys: API authentication tokens
Relationships
users โโ< projects โโ< test_runs
โ โ
โ โโ project_settings
โ
โโ< organization_members >โ organizations
๐ Authentication Flow
graph LR
A[Client] --> B{Auth Type}
B -->|Session| C[Cookie]
B -->|API Token| D[Bearer Token]
C --> E[Validate Session]
D --> F[Validate Token]
E --> G[User Context]
F --> G
G --> H[tRPC Procedures]
๐ Example Implementation
tRPC Procedure
// packages/trpc/src/routers/index.ts
import { z } from 'zod'
import { publicProcedure, router } from '../trpc'
import { projects } from '@izri/database'
import { eq } from 'drizzle-orm'
export const appRouter = router({
projects: router({
// Query example
getProject: protectedProcedure
.input(z.object({
projectId: z.string(),
userEmail: z.string().email()
}))
.query(async ({ ctx, input }) => {
const project = await ctx.db.query.projects.findFirst({
where: eq(projects.id, input.projectId),
with: {
testRuns: {
orderBy: (testRuns, { desc }) => [desc(testRuns.createdAt)],
limit: 10
}
}
})
if (!project) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Project not found'
})
}
return project
}),
// Mutation example
createProject: protectedProcedure
.input(z.object({
name: z.string(),
description: z.string().optional(),
repository: z.string().url(),
branch: z.string().default('main'),
language: z.string(),
framework: z.string().optional(),
userEmail: z.string().email()
}))
.mutation(async ({ ctx, input }) => {
const [project] = await ctx.db
.insert(projects)
.values({
...input,
userId: ctx.user.id
})
.returning()
return project
})
})
})
Using from Frontend
// In React component
import { trpc } from '~/lib/trpc'
function ProjectDetail({ projectId }: { projectId: string }) {
// Query - auto-typed!
const { data: project, isLoading } = trpc.projects.getProject.useQuery({
projectId,
userEmail: user.email
})
// Mutation - auto-typed!
const updateProject = trpc.projects.updateProject.useMutation({
onSuccess: () => {
// Handle success
}
})
if (isLoading) return <div>Loading...</div>
return (
<div>
<h1>{project.name}</h1>
<p>{project.description}</p>
{/* ... */}
</div>
)
}
๐ง Configuration
Environment Variables
# Database
DATABASE_URL="postgresql://postgres:password@localhost:5433/izri"
# API Server
API_PORT=4000
LOG_LEVEL="info" # debug, info, warn, error
# OpenTelemetry (optional)
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318/v1/traces"
# Authentication (future)
JWT_SECRET="your-secret-key"
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"
Hono Server Configuration
// apps/api/src/index.ts
import { Hono } from 'hono'
import { trpcServer } from '@hono/trpc-server'
import { appRouter } from '@izri/trpc'
import { createContext } from '@izri/trpc/context'
const app = new Hono()
// Health endpoint
app.get('/health', (c) => c.json({ ok: true }))
// tRPC endpoint
app.use(
'/trpc/*',
trpcServer({
router: appRouter,
createContext
})
)
const port = process.env.API_PORT || 4000
console.log(`๐ Server running on http://localhost:${port}`)
export default app
๐ Performance
Query Optimization
// Use indexes
await ctx.db.query.projects.findMany({
where: eq(projects.userId, userId),
// Fast due to index on userId
})
// Use select for large tables
await ctx.db
.select({
id: projects.id,
name: projects.name
})
.from(projects)
.where(eq(projects.userId, userId))
// Use with for relations
await ctx.db.query.projects.findFirst({
with: {
testRuns: true // Efficient join
}
})
Caching Strategy
// Redis caching (future)
const cacheKey = `project:${projectId}`
const cached = await redis.get(cacheKey)
if (cached) return JSON.parse(cached)
const project = await db.query.projects.findFirst({...})
await redis.set(cacheKey, JSON.stringify(project), 'EX', 3600)
๐งช Testing
// Test tRPC procedures
import { appRouter } from './routers'
import { createContext } from './context'
describe('projects.getProject', () => {
it('should return project', async () => {
const ctx = await createContext({})
const caller = appRouter.createCaller(ctx)
const project = await caller.projects.getProject({
projectId: 'test-project-id',
userEmail: 'test@example.com'
})
expect(project).toBeDefined()
expect(project.id).toBe('test-project-id')
})
})
๐ Related Documentation
- Architecture: System Overview
- Frontend: tRPC Integration
- Database: Database Package
- API Reference: tRPC Procedures
๐ Troubleshooting
Server Won't Start
# Check if port is in use
lsof -ti:4000 | xargs kill -9
# Check database connection
psql $DATABASE_URL
# Check environment variables
cat .env | grep DATABASE_URL
Type Errors
# Rebuild packages
pnpm build:packages
# Generate database types
pnpm db:generate
Database Errors
# Check migrations
pnpm db:migrate
# Restart database
pnpm docker:down
pnpm dev:docker
Ready to dive deep? Start with API Overview โ