Fusite API Low-Level Design

Design Documents
Last updated August 23, 2025

Fusite API Low-Level Design

Based on the Fusite High-Level Design document, this low-level design outlines the API implementation for a content management service that exposes Notion data to other applications.

1. Overview

The Fusite API serves as a content management API that:

  1. Shared Notion Access: Uses the fusite-commons package for Notion SDK access
  2. RESTful Endpoints: Provides structured data access t#### fuschronicleController.ts
typescript
import { ChronicleService } from 'fusite-commons';
    
    export class FuschronicleController {r applications
    3. **Content Transformation**: Converts Notion blocks to API-ready format
    4. **Webhook Handler**: Responds to Notion content changes for real-time updates
    
    The API is built with:
    
    - **Runtime**: Node.js 22.x for Lambda compatibility
    - **Framework**: Express.js with TypeScript
    - **Deployment**: AWS Lambda via Serverless Framework
    - **Data Source**: Shared fusite-commons package (same as Web)
    - **Consumers**: Other projects like fuscorrespondence, fuscapp, etc.
    
    ## 2. Project Structure
    

/api
bunfig.toml # Bun configuration
package.json # Dependencies and scripts (includes fusite-commons)
serverless.yml # AWS deployment configuration
tsconfig.json # TypeScript configuration
/src
/controllers # API endpoint controllers
projectController.ts # Project CRUD operations
taskController.ts # Task CRUD operations
chronicleController.ts # Fuschronicle CRUD operations
aboutController.ts # About page operations
/transformers # API-specific transformers
notionToApi.ts # Convert shared commons data to API format
responseFormatter.ts # Format API responses consistently
/webhook # Webhook handling
handler.ts # Notion webhook processor
validator.ts # Webhook signature validation
/middleware # Express middleware
auth.ts # Authentication middleware
cors.ts # CORS configuration
validation.ts # Request validation
rateLimit.ts # Rate limiting
/routes # API route definitions
projects.ts # Project routes
tasks.ts # Task routes
fuschronicles.ts # Fuschronicle routes
about.ts # About routes
webhook.ts # Webhook routes
/utils # API-specific utilities
config.ts # API environment configuration
logger.ts # Structured logging
errors.ts # Custom error classes
app.ts # Express application setup
lambdaHandler.ts # AWS Lambda entry point
/tests # Test files
/unit # Unit tests
/integration # Integration tests
/fixtures # Test data

/commons # Shared package between API and Web
package.json # Commons package definition
index.ts # Main exports
/src
/notion # Notion SDK access layer
client.ts # Notion SDK initialization
repositories/ # Data access layer
project.ts # Project database operations
task.ts # Task database operations
chronicle.ts # Chronicle database operations
about.ts # About page operations
services/ # Business logic layer
projectService.ts # Project business logic
taskService.ts # Task business logic
chronicleService.ts # Chronicle business logic
contentService.ts # Content transformation logic
transformers/ # Base data transformers
notionToBase.ts # Convert Notion blocks to base format
slugGenerator.ts # Generate and validate slugs
types.ts # Shared type definitions
/utils # Shared utilities
validation.ts # Input validation schemas
errors.ts # Custom error classes

  /utils                 # Shared utilities
    config.ts            # Environment configuration
    logger.ts            # Structured logging
    errors.ts            # Custom error classes
    validation.ts        # Input validation schemas
    app.ts                 # Express application setup
    lambdaHandler.ts       # AWS Lambda entry point
    /tests                   # Test files
    /unit                  # Unit tests
    /integration           # Integration tests
    /fixtures              # Test data

3. Shared Architecture with Commons Package

3.1 Commons Package Integration

The API leverages the fusite-commons package for all Notion data access, ensuring consistency with the Web project:

typescript
// API package.json dependency
    {
    "dependencies": {
    "fusite-commons": "workspace:*",
    "express": "^4.18.0",
    "cors": "^2.8.5",
    // ... other API-specific dependencies
    }
    }

3.2 Shared Components Usage

typescript
// API controllers import shared services
    import {
    ProjectService,
    ChronicleService,
    TaskService,
    AboutService,
    Project,
    Fuschronicle,
    ProjectTask,
    } from "fusite-commons"
    
    // Controllers focus on HTTP handling and API formatting
    export class ProjectController {
    constructor(private projectService: ProjectService) {}
    
    async getProjects(req: Request, res: Response) {
    // Use shared business logic
    const projects = await this.projectService.getPublicProjects()
    
    // Transform to API format
    const apiProjects = projects.map(this.transformToApiFormat)
    
    res.json({ data: apiProjects })
    }
    }

3.3 Benefits of Shared Architecture

  • Consistency: Same data access logic across API and Web
  • Maintainability: Single source of truth for business logic
  • Efficiency: Shared validation, transformers, and types
  • Testing: Test business logic once in commons package

4. Data Models and Notion Schema

4.1 Notion Database Mappings

Based on the high-level design, the API works with these Notion databases:

A) About Me (Single Page)

typescript
// Data models are defined in fusite-commons package
    import {
    AboutPage,
    Project,
    ProjectTask,
    Fuschronicle,
    ProjectStatus,
    TaskStatus,
    ChronicleType,
    NotionBlock,
    } from "fusite-commons"

4.2 API Response Types

typescript
// Standard API response wrapper
    interface ApiResponse<T> {
    success: boolean
    data?: T
    meta?: ResponseMeta
    timestamp: string
    }
    
    interface ErrorResponse {
    success: false
    error: {
    code: string
    message: string
    details?: any
    }
    timestamp: string
    }
    
    interface ResponseMeta {
    total?: number
    page?: number
    pageSize?: number
    hasMore?: boolean
    }
    
    // Transformed content for API consumption
    interface ApiContent {
    type:
    | "heading_1"
    | "heading_2"
    | "heading_3"
    | "paragraph"
    | "bulleted_list_item"
    | "numbered_list_item"
    | "code"
    | "quote"
    | "image"
    | "video"
    | "file"
    content: string
    annotations?: {
    bold?: boolean
    italic?: boolean
    strikethrough?: boolean
    underline?: boolean
    code?: boolean
    color?: string
    }
    href?: string
    src?: string // for images/videos/files
    alt?: string // for images
    caption?: string
    }
    
    // Project with API-friendly format
    interface ApiProject extends Omit<Project, "content"> {
    content: ApiContent[]
    metrics?: ProjectMetrics
    related_chronicles?: Fuschronicle[]
    }
    
    // Chronicle with API-friendly format
    interface ApiChronicle extends Omit<Fuschronicle, "content" | "projects"> {
    content: ApiContent[]
    projects: Pick<Project, "id" | "name" | "slug" | "logo">[]
    }
    
    // Task with API-friendly format
    interface ApiTask extends Omit<ProjectTask, "content" | "project"> {
    content: ApiContent[]
    project: Pick<Project, "id" | "name" | "slug">
    }
    
    interface ProjectMetrics {
    totalTasks: number
    completedTasks: number
    totalEstimatedTime: number
    totalActualTime: number
    firstTaskDate?: string
    lastTaskDate?: string
    completionRate: number
    averageTaskTime: number
    }

5. Core Components

5.1 Shared Notion Data Access

The API uses the fusite-commons package for all Notion data access, providing:

  • Repositories: Project, Task, Chronicle, About page data access
  • Services: Business logic and content transformation
  • Transformers: Convert Notion blocks to base format
  • Types: Shared data structures across API and Web
typescript
// Import shared components
    import {
    ProjectService,
    ChronicleService,
    TaskService,
    AboutService,
    } from "fusite-commons"
    
    class ProjectController {
    constructor(
    private projectService: ProjectService,
    private taskService: TaskService
    ) {}
    
    async getProjects(req: Request, res: Response) {
    const projects = await this.projectService.getPublicProjects()
    const apiProjects = projects.map(this.transformToApiFormat)
    res.json({ data: apiProjects })
    }
    }

5.2 API-Specific Transformers (/src/transformers/)

notionToApi.ts

typescript
import { Project, Fuschronicle } from "fusite-commons"
    
    export class NotionToApiTransformer {
    transformProject(project: Project): ApiProject {
    return {
    id: project.id,
    name: project.name,
    slug: project.slug,
    description: project.description,
    status: project.status,
    visibility: project.visibility,
    tags: project.tags,
    repository: project.repository,
    demo: project.demo,
    createdAt: project.createdAt,
    updatedAt: project.updatedAt,
    // Transform content for API consumption
    content: this.transformContent(project.content),
    }
    }
    
    transformChronicle(chronicle: Fuschronicle): ApiChronicle {
    return {
    id: chronicle.id,
    title: chronicle.title,
    slug: chronicle.slug,
    excerpt: chronicle.excerpt,
    status: chronicle.status,
    projectId: chronicle.projectId,
    publishedAt: chronicle.publishedAt,
    createdAt: chronicle.createdAt,
    updatedAt: chronicle.updatedAt,
    content: this.transformContent(chronicle.content),
    }
    }
    
    private transformContent(content: any[]): ApiContent[] {
    // Transform notion blocks to API-friendly format
    return content.map((block) => this.transformBlock(block))
    }
    }

responseFormatter.ts

typescript
export class ResponseFormatter {
    formatSuccessResponse<T>(data: T, meta?: ResponseMeta): ApiResponse<T> {
    return {
    success: true,
    data,
    meta,
    timestamp: new Date().toISOString(),
    }
    }
    
    formatErrorResponse(error: ApiError): ErrorResponse {
    return {
    success: false,
    error: {
    code: error.code,
    message: error.message,
    details: error.details,
    },
    timestamp: new Date().toISOString(),
    }
    }
    }

5.3 Controllers (/src/controllers/)

projectController.ts

typescript
import { ProjectService } from "fusite-commons"
    
    export class ProjectController {
    constructor(
    private projectService: ProjectService,
    private transformer: NotionToApiTransformer,
    private formatter: ResponseFormatter
    ) {}
    
    async getAllProjects(req: Request, res: Response): Promise<void> {
    try {
    const filters = this.parseProjectFilters(req.query)
    const projects = await this.projectService.getPublicProjects(filters)
    const apiProjects = projects.map((p) =>
    this.transformer.transformProject(p)
    )
    
    res.json(this.formatter.formatSuccessResponse(apiProjects))
    } catch (error) {
    res.status(500).json(this.formatter.formatErrorResponse(error))
    }
    }
    
    async getProjectBySlug(req: Request, res: Response): Promise<void> {
    try {
    const { slug } = req.params
    const project = await this.projectService.getProjectBySlug(slug)
    
    if (!project) {
    return res
    .status(404)
    .json(
    this.formatter.formatErrorResponse(
    new NotFoundError("Project not found")
    )
    )
    }
    
    const apiProject = this.transformer.transformProject(project)
    res.json(this.formatter.formatSuccessResponse(apiProject))
    } catch (error) {
    res.status(500).json(this.formatter.formatErrorResponse(error))
    }
    }
    
    private parseProjectFilters(query: any): ProjectFilters {
    return {
    status: query.status?.split(","),
    tags: query.tags?.split(","),
    search: query.search,
    }
    }
    }

chronicleController.ts

typescript
import { ChronicleService } from "fusite-commons"
    
    export class FuschronicleController {
    constructor(
    private chronicleService: ChronicleService,
    private transformer: NotionToApiTransformer,
    private formatter: ResponseFormatter
    ) {}
    
    async getAllFuschronicles(req: Request, res: Response): Promise<void> {
    try {
    const chronicles = await this.chronicleService.getPublishedChronicles()
    const apiChronicles = chronicles.map((c) =>
    this.transformer.transformChronicle(c)
    )
    
    res.json(this.formatter.formatSuccessResponse(apiChronicles))
    } catch (error) {
    res.status(500).json(this.formatter.formatErrorResponse(error))
    }
    }
    
    async getFuschronicleBySlug(req: Request, res: Response): Promise<void> {
    try {
    const { slug } = req.params
    const chronicle = await this.chronicleService.getChronicleBySlug(slug)
    
    if (!chronicle) {
    return res
    .status(404)
    .json(
    this.formatter.formatErrorResponse(
    new NotFoundError("Fuschronicle not found")
    )
    )
    }
    
    const apiChronicle = this.transformer.transformChronicle(chronicle)
    res.json(this.formatter.formatSuccessResponse(apiChronicle))
    } catch (error) {
    res.status(500).json(this.formatter.formatErrorResponse(error))
    }
    }
    }

5.4 Webhook Handling (/src/webhook/)

handler.ts

typescript
import { ProjectService, ChronicleService } from 'fusite-commons';
    
    export class WebhookHandler {
    constructor(
    private validator: WebhookValidator,
    private projectService: ProjectService,
    private chronicleService: ChronicleService
    ) {}
    
    async handleNotionWebhook(
    payload: NotionWebhookPayload
    ): Promise<WebhookResponse> {
    try {
    // Validate webhook signature
    if (!this.validator.validateSignature(payload)) {
    throw new WebhookValidationError('Invalid webhook signature');
    }
    
    // Process webhook based on event type
    switch (payload.event_type) {
    case 'page.updated':
    await this.handlePageUpdate(payload);
    break;
    case 'page.created':
    await this.handlePageCreated(payload);
    break;
    case 'page.deleted':
    await this.handlePageDeleted(payload);
    break;
    default:
    console.log(`Unhandled webhook event: ${payload.event_type}`);
    }
    
    return { success: true, processed: true };
    } catch (error) {
    console.error('Webhook processing error:', error);
    return { success: false, error: error.message };
    }
    }
    
    private async handlePageUpdate(payload: NotionWebhookPayload): Promise<void> {
    // Clear any caches or trigger regeneration if needed
    // Since we don't cache, this might just log the event
    console.log(`Page updated: ${payload.page_id}`);
    }
    }
    private async processPageChange(change: NotionPageChange): Promise<void>
    private async logContentChange(pageId: string, type: string): Promise<void>
    }
    
    interface NotionWebhookPayload {
    object: string
    event_type: "page.property_changed" | "page.created" | "page.deleted"
    page_id: string
    workspace_id: string
    timestamp: string
    }
    
    interface WebhookResponse {
    success: boolean
    processed: boolean
    message?: string
    }

6. API Endpoints

6.1 Project Endpoints

GET /api/projects
    - Query params:
    - status: ProjectStatus[]
    - tags: string[]
    - search: string
    - Returns: ApiResponse<ApiProject[]>
    
    GET /api/projects/{id}
    - Returns: ApiResponse<ApiProject>
    
    GET /api/projects/{id}/tasks
    - Query params: status
    - Returns: ApiResponse<ApiTask[]>
    

6.2 Fuschronicle Endpoints

GET /api/fuschronicles
    - Query params:
    - type: ChronicleType[]
    - projectIds: string[]
    - dateFrom: string (ISO date)
    - dateTo: string (ISO date)
    - search: string
    - Returns: ApiResponse<ApiChronicle[]>
    
    GET /api/fuschronicles/{slug}
    - Returns: ApiResponse<ApiChronicle>
    

6.3 Task Endpoints

GET /api/tasks
    - Query params:
    - projectId: string
    - status: TaskStatus[]
    - dateFrom: string
    - dateTo: string
    - Returns: ApiResponse<ApiTask[]>
    
    GET /api/tasks/{id}
    - Returns: ApiResponse<ApiTask>
    

6.4 About Endpoints

GET /api/about
    - Returns: ApiResponse<AboutPage>

6.5 Webhook Endpoints

POST /api/webhook/notion
    - Handles Notion webhook events
    - Validates signature
    - Invalidates relevant cache
    - Returns: WebhookResponse
    
    GET /api/webhook/health
    - Health check for webhook endpoint
    - Returns: { status: "ok", timestamp: string }

6.6 Utility Endpoints

GET /api/health
    - System health check
    - Returns: { status: "ok", services: ServiceStatus[] }

7. Configuration and Environment

6.1 Environment Variables

bash
# Notion Configuration
    NOTION_TOKEN=secret_xxx
    NOTION_DATABASE_ABOUT=xxx
    NOTION_DATABASE_PROJECT=xxx
    NOTION_DATABASE_TASK=xxx
    NOTION_DATABASE_CHRONICLE=xxx
    
    # API Configuration
    API_GATEWAY_ID=aws_api_gateway_id
    API_BASE_URL=https://api.fuscripts.com
    CORS_ORIGINS=https://fuscripts.com,https://*.fuscripts.com
    
    # Webhook Configuration
    WEBHOOK_SECRET=xxx

6.2 Serverless Configuration

Key aspects of serverless.yml:

yaml
functions:
    # Main API handler
    api:
    handler: src/lambdaHandler.handler
    timeout: 30
    memorySize: 128
    events:
    - httpApi:
    path: /api/{proxy+}
    method: ANY
    environment:
    NODE_ENV: ${env:NODE_ENV}
    
    # Webhook handler (separate for fast response)
    webhook:
    handler: src/webhook/lambdaHandler.handler
    timeout: 30
    memorySize: 128
    events:
    - httpApi:
    path: /webhook/notion
    method: POST

8. Error Handling and Monitoring

7.1 Error Types

typescript
export class FusiteApiError extends Error {
    constructor(
    message: string,
    public code: string,
    public statusCode: number = 500,
    public details?: any
    ) {
    super(message)
    }
    }
    
    export class NotionError extends FusiteApiError {
    constructor(message: string, details?: any) {
    super(message, "NOTION_ERROR", 503, details)
    }
    }
    
    export class ValidationError extends FusiteApiError {
    constructor(message: string, details?: any) {
    super(message, "VALIDATION_ERROR", 400, details)
    }
    }
    
    export class NotFoundError extends FusiteApiError {
    constructor(resource: string) {
    super(`${resource} not found`, "NOT_FOUND", 404)
    }
    }
    
    export class AuthenticationError extends FusiteApiError {
    constructor(message: string = "Authentication required") {
    super(message, "AUTH_ERROR", 401)
    }
    }
    
    export class RateLimitError extends FusiteApiError {
    constructor() {
    super("Rate limit exceeded", "RATE_LIMIT", 429)
    }
    }

7.2 Logging

typescript
export class Logger {
    info(message: string, meta?: any): void
    warn(message: string, meta?: any): void
    error(message: string, error?: Error, meta?: any): void
    debug(message: string, meta?: any): void
    
    // Structured logging for specific events
    apiRequest(
    method: string,
    path: string,
    statusCode: number,
    duration: number
    ): void
    notionApiCall(
    method: string,
    endpoint: string,
    duration: number,
    success: boolean
    ): void
    webhookReceived(eventType: string, pageId: string): void
    authAttempt(success: boolean, method: string): void
    }

9. Testing Strategy

8.1 Test Structure

/tests
    /unit
    /controllers    # Controller unit tests
    /services       # Service layer tests
    /notion         # Notion repository tests
    /transformers   # Data transformation tests
    /webhook        # Webhook handling tests
    /integration
    /api            # API endpoint integration tests
    /notion         # End-to-end Notion integration tests
    /fixtures
    notion-responses.json    # Mock Notion API responses
    expected-api-data.json   # Expected API response formats
    webhook-payloads.json    # Sample webhook payloads

8.2 Key Test Scenarios

  1. API Endpoint Testing

    • Request/response validation
    • Authentication and authorization
    • Error handling
  2. Notion Data Access

    • Mock Notion API responses
    • Data transformation accuracy
    • Error handling for API failures
  3. Content Transformation

    • Notion blocks to API format conversion
    • Image processing and URL rewriting
    • Plain text extraction
  4. Webhook Processing

    • Signature validation
    • Content change logging
    • Error recovery

10. Performance Considerations

9.1 Optimization Strategies

  1. Database Optimization

    • Notion API rate limiting compliance
    • Connection pooling
    • Batch operations where possible
    • Efficient query filtering
  2. API Performance

    • Response compression
    • Selective field loading
    • Async processing for heavy operations
  3. Lambda Optimization

    • Connection reuse across invocations
    • Minimal cold start dependencies
    • Memory optimization based on usage patterns

9.2 Monitoring Metrics

  • API response times by endpoint
  • Notion API response times and error rates
  • Memory usage and Lambda duration
  • Request rate and error rate by consumer
  • Webhook processing latency

11. Security Considerations

11.1 Access Control

  • This API is public by default

11.2 Content Security

  • Input validation for all request data
  • Notion data sanitization

12. Summary

This low-level design provides a comprehensive blueprint for implementing the Fusite API that:

  • Shares Core Logic: Uses the fusite-commons package for all Notion data access and business logic
  • RESTful Architecture: Provides clean, consistent API endpoints following REST conventions
  • Serves Multiple Consumers: Designed to serve other internal projects like fuscorrespondence and fuscapp
  • Scalable and Maintainable: Serverless architecture with shared commons package reduces duplication
  • Real-time Updates: Webhook support for immediate content synchronization

The API serves as a thin HTTP layer over the shared business logic, transforming commons data models into API-ready format while handling authentication, validation, rate limiting, and webhooks. This architecture enables both the API and Web projects to share the same reliable data access layer while serving their distinct purposes.