API Architecture

📑 Table of Contents

📖 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

  1. Domain-Driven Design: Clear separation of business domains
  2. Layered Architecture: Controllers → Operations → Database
  3. Type Safety: End-to-end TypeScript with Zod validation
  4. API-First: All functionality exposed through well-documented APIs
  5. 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

  1. Client Request

    • Request initiated with HTTP method, headers, and body
    • Includes authentication tokens, content type, and other metadata
  2. 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
  3. 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
  4. Operations Layer

    • Execute business logic and domain rules
    • Perform database queries and transactions
    • Validate business constraints and invariants
    • Coordinate multiple features if needed
  5. 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:

Examples:

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:

Examples:

📖 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:

Workflow Benefits:

Combined Benefits:

🔧 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

  1. Authentication: JWT tokens with refresh mechanism
  2. Authorization: Role-based access control
  3. Input Validation: Zod schema validation for all inputs
  4. SQL Injection Prevention: Parameterized queries via Drizzle ORM
  5. Rate Limiting: Per-endpoint rate limiting
  6. CORS: Proper cross-origin resource sharing
  7. 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

  1. Structured Logging: JSON format with consistent fields
  2. Log Levels: Error, warn, info, debug
  3. Request Tracking: Unique request IDs for tracing
  4. Performance Metrics: Response time tracking
  5. 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

🚀 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


🔗 Related Documentation