API Architecture
📑 Table of Contents
- 📖 Overview
- 🏗️ System Design
- 🎯 Core Patterns
- 🔄 Request Flow
- 🎭 Features vs Workflows
- 🔧 Development Patterns
- 🛡️ Security & Validation
- 📊 Monitoring & Logging
📖 Overview
The Ivyi API is a robust, scalable backend service built with Express.js and TypeScript, following domain-driven design principles. It provides RESTful endpoints for all platform functionality with comprehensive error handling, validation, and documentation.
🏗️ System Design
Architecture Principles
- Domain-Driven Design: Clear separation of business domains
- Layered Architecture: Controllers → Operations → Database
- Type Safety: End-to-end TypeScript with Zod validation
- API-First: All functionality exposed through well-documented APIs
- JSON:API: Standardized response format for consistency
🎯 Core Patterns
1. Three-Layer Architecture
Controllers Layer
// Handle HTTP requests/responses
export const createGiftController = asyncHandler(
async (req: Request, res: Response) => {
// Extract request data
// Call operations layer
// Handle response formatting
// Error handling
},
);
📖 See Controllers Documentation - Complete guide to all API controllers
Operations Layer
// Business logic and data operations
export const createGift = async (giftData: CreateGiftRequest) => {
// Validation
// Database operations
// Business rules
// Error handling
};
📋 See Operations Documentation - Complete guide to all business logic operations
Database Layer
// Drizzle schemas and database interactions
export const giftsSchema = pgTable("gifts", {
// Schema definition
// Relationships
// Constraints
});
2. JSON:API Response Format
All API responses follow JSON:API specification:
// Single Resource Response
{
"data": {
"type": "gift",
"id": "uuid",
"attributes": { /* resource data */ }
}
}
// Collection Response
{
"data": [
{
"type": "gift",
"id": "uuid",
"attributes": { /* resource data */ }
}
]
}
3. Type Safety with Zod
Request/response validation with Zod schemas:
// Request validation
const createGiftRequestSchema = z.object({
title: z.string().min(1),
description: z.string().optional(),
price: z.number().positive(),
});
// Response serialization
const SerializedGift = createSelectSchema(giftsSchema).openapi({
title: "Gift",
description: "A gift item",
});
🔄 Request Flow
Typical Request Lifecycle
-
Client Request
- Request initiated with HTTP method, headers, and body
- Includes authentication tokens, content type, and other metadata
-
Middleware Processing
- Authentication: Verify user identity and permissions
- CORS: Handle cross-origin requests
- Rate Limiting: Prevent abuse and ensure fair usage
- Request validation: Basic format and structure checks
-
Controller Layer
- Validate incoming request data against schemas
- Extract parameters from route, query, and body
- Call appropriate operations layer functions
- Handle response formatting and error cases
-
Operations Layer
- Execute business logic and domain rules
- Perform database queries and transactions
- Validate business constraints and invariants
- Coordinate multiple features if needed
-
Response Flow
- Operations return data to controller
- Controller formats response using JSON:API specification
- Includes appropriate status codes and headers
Error Handling Flow
try {
// Business logic
const result = await operation(data);
return sendSuccessResponse(req, res, result, { status: HttpStatus.CREATED });
} catch (error) {
console.error("Operation failed:", error);
return sendErrorResponse(req, res, {
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: "Operation failed",
});
}
🎭 Features vs Workflows
Features: Scoped Business Domains
Features are self-contained business domains with clear boundaries:
feature/[domain]/
├── [domain].schema.ts # Database models
├── operations/ # Business logic
├── controllers/ # HTTP handlers
├── [domain].routes.ts # Route definitions
└── [domain].docs.ts # API documentation
Characteristics:
- Single Responsibility: Each feature handles one business domain
- Self-Contained: All logic, models, and routes in one place
- Independent: Can be developed and tested in isolation
- Clear API: Exposes RESTful endpoints for its domain
Examples:
- Users Feature: User management, profiles, authentication
- Gifts Feature: Gift catalog, categories, metadata
- Relationships Feature: User connections, affinity levels
- Classifier Feature: Question-based classification system
Workflows: Cross-Feature Business Processes
Workflows orchestrate multiple features to achieve complex business goals:
workflows/[process]/
├── [process].schema.ts # Workflow state models
├── [process].controller.ts # Workflow orchestration
├── operations/ # Workflow-specific operations
└── events/ # Workflow events and triggers
Characteristics:
- Cross-Domain: Coordinate multiple features
- State Management: Track workflow progress and state
- Event-Driven: Emit events for workflow transitions
- Business Orchestration: Complex multi-step processes
Examples:
-
Gifting Process:
- Uses Users (recipient, sender)
- Uses Relationships (relationship context)
- Uses Gifts (suggestions, selection)
- Uses Classifier (personalization)
- Uses Decisions (recommendation logic)
-
Onboarding Workflow:
- Uses Users (account creation)
- Uses Relationships (initial connections)
- Uses Classifier (preference gathering)
📖 See Workflows Documentation - Complete guide to workflow orchestration and state management
Interaction Patterns
Feature-to-Feature Communication
// Feature operations can call other feature operations
import { findUserById } from "../users/operations/users.find";
import { createRelationship } from "../relationships/operations/relationships.create";
export const createUserWithRelationship = async (
userData: UserData,
relationshipData: RelationshipData,
) => {
const user = await findUserById(userData.id);
const relationship = await createRelationship({
...relationshipData,
userId: user.id,
});
return { user, relationship };
};
Workflow Orchestration
// Workflows coordinate multiple features
import { createGiftProcess } from "./operations/gifting-process.create";
import { getGiftSuggestions } from "../gifts/operations/gifts.find";
import { classifyUserPreferences } from "../classifier/operations/classifier.find";
export const startGiftingProcess = async (processData: GiftingProcessData) => {
// Create workflow state
const process = await createGiftProcess(processData);
// Coordinate multiple features
const userPreferences = await classifyUserPreferences(
processData.recipientId,
);
const suggestions = await getGiftSuggestions(userPreferences);
// Update workflow state
await updateGiftProcess(process.id, {
status: "OPTIONS_GENERATED",
suggestions: suggestions,
});
return process;
};
Benefits of This Architecture
Feature Benefits:
- Clear Boundaries: Easy to understand feature responsibilities
- Independent Development: Teams can work on features in parallel
- Testable: Each feature can be unit tested independently
- Maintainable: Changes are isolated to specific domains
Workflow Benefits:
- Business Process Focus: Workflows represent real business processes
- Cross-Feature Coordination: Orchestrate complex multi-domain operations
- State Management: Track progress through multi-step processes
- Event Integration: React to workflow events and transitions
Combined Benefits:
- Scalability: Features can be added without affecting workflows
- Flexibility: Workflows can be reconfigured to use different features
- Separation of Concerns: Business logic separated from orchestration
- Testability: Features and workflows can be tested independently
🔧 Development Patterns
1. Async Handler Pattern
export const createGiftController = asyncHandler(
async (req: Request, res: Response) => {
// Controller logic here
},
);
📖 See Controllers Documentation - Complete guide to all API controllers and handler patterns
2. Serialization Pattern
// Define serializer config
export const GiftSerializer: JsonApiResourceConfig<Gift> = {
type: "single",
attributes: (gift: Gift) => ({
id: gift.id,
title: gift.title,
description: gift.description,
price: gift.price,
}),
};
// Use in controller
return sendSuccessResponse(req, res, {
data: result,
serializerConfig: GiftSerializer,
type: "single",
});
3. Database Transaction Pattern
export const createGiftWithImages = async (
giftData: GiftData,
images: ImageData[],
) => {
return await db.transaction(async (tx) => {
const gift = await tx.insert(giftsSchema).values(giftData).returning();
await tx
.insert(giftImagesSchema)
.values(images.map((img) => ({ ...img, giftId: gift[0].id })));
return gift[0];
});
};
📋 See Operations Documentation - Complete guide to all business logic operations and transaction patterns
4. Validation Pattern
// Schema validation
const createGiftSchema = z.object({
title: z.string().min(1).max(255),
description: z.string().max(1000),
price: z.number().positive().max(10000),
});
// Controller validation
const validatedData = createGiftSchema.parse(req.body);
🛡️ Security & Validation
Security Measures
- Authentication: JWT tokens with refresh mechanism
- Authorization: Role-based access control
- Input Validation: Zod schema validation for all inputs
- SQL Injection Prevention: Parameterized queries via Drizzle ORM
- Rate Limiting: Per-endpoint rate limiting
- CORS: Proper cross-origin resource sharing
- Security Headers: HSTS, CSP, and other security headers
Input Validation
// Request body validation
const createGiftRequestSchema = z.object({
title: z.string().min(1, "Title is required").max(255, "Title too long"),
description: z.string().max(1000, "Description too long").optional(),
price: z
.number()
.positive("Price must be positive")
.max(10000, "Price too high"),
});
// Query parameter validation
const getGiftsQuerySchema = z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(100).default(20),
category: z.string().optional(),
});
Error Handling
// Standardized error responses
export const sendErrorResponse = (
req: Request,
res: Response,
error: ErrorResponse,
) => {
res.status(error.status).json({
errors: [
{
status: error.status.toString(),
title: error.title,
detail: error.description,
},
],
});
};
📊 Monitoring & Logging
Logging Strategy
- Structured Logging: JSON format with consistent fields
- Log Levels: Error, warn, info, debug
- Request Tracking: Unique request IDs for tracing
- Performance Metrics: Response time tracking
- Error Aggregation: Centralized error reporting
Health Checks
// Health check endpoint
app.get("/health", async (req, res) => {
const health = {
status: "healthy",
timestamp: new Date().toISOString(),
services: {
database: await checkDatabaseHealth(),
redis: await checkRedisHealth(),
},
};
res.json(health);
});
Performance Monitoring
- Response Time Tracking: Per-endpoint performance
- Database Query Monitoring: Slow query detection
- Memory Usage: Application memory tracking
- Error Rate Monitoring: Error frequency and patterns
🚀 API Documentation
OpenAPI Integration
All endpoints are documented with OpenAPI/Swagger:
// Register endpoint documentation
classifierRegistry.registerPath({
method: "post",
path: "/v1/classifier/questions",
summary: "Create classifier question",
description: "Creates a new classifier question",
tags: ["Classifier v1"],
security: [{ bearerAuth: [] }],
request: {
body: {
description: "Question data to create",
required: true,
content: {
"application/json": {
schema: createSelectSchema(giftClassifierQuestionsSchema),
},
},
},
},
responses: {
[HttpStatus.CREATED.statusCode]: {
description: HttpStatus.CREATED.description,
content: {
"application/json": {
schema: questionSingleResponseSchema,
},
},
},
},
});
Interactive Documentation
- Swagger UI: Interactive API exploration
- Request Examples: Sample requests for each endpoint
- Response Examples: Expected response formats
- Authentication Guide: How to authenticate requests
- Error Documentation: All possible error responses
🔗 Related Documentation
- 🏗️ Platform Architecture - Complete system overview
- 🚀 Queues & Jobs - Background processing
- 👷 Workers & Events - Event-driven architecture
- 📖 Frontend Architecture - Frontend system design