Live
fuschimp_transparent.png
Fusite

My little corner of the internet showcasing personal projects and updates about my work

Node.js Serverless Handlebars Bun AWS

API Tech Design Doc

Overview

Fusite API is a serverless Express API that serves my projects' metadata from a Notion DB. Deployed on AWS Lambda with API Gateway, it provides read-only access to project portfolios, development chronicles, and documentation—all managed through Notion's collaborative interface.

This approach combines the developer experience of a traditional database with the content editing simplicity of Notion.

This API is used by the Fuscapp Hub to list my current available projects.


Core Architecture

Serverless Express Pattern

plain text
Client Request
    ↓
API Gateway (HTTP API)
    ↓
AWS Lambda (Node.js 22)
    ↓
serverless-http adapter
    ↓
Express Application
    ├── CORS Middleware
    ├── Health Check
    └── Project Controller
        ↓
    NotionService (fusite-commons)
        ↓
    Notion API

Key Technology Choices:

  • Express 4.x: Familiar HTTP routing and middleware
  • serverless-http: Bridges Express to Lambda's event model
  • Serverless Framework: Infrastructure-as-code for deployment
  • TypeScript 5.8+: Full type safety with strict mode
  • The architecture follows a thin API gateway pattern—Express handles HTTP semantics while Lambda provides compute. This separation enables horizontal scaling without server management.


    The Notion-as-Database Pattern

    Why Notion?

    Traditional CMSs require admin panels, migrations, and complex schemas. Notion provides:

  • Relational Databases: Native support for linked content (projects ↔ chronicles)
  • Flexible Schema: Add properties without migrations
  • Collaborative Editing: Share databases with team members
  • Rich Content: Native support for code blocks, images, embeds, formatted text
  • Mobile Editing: Update content from anywhere via Notion's mobile app
  • Type System & Contracts

    Domain Models

    The system defines comprehensive TypeScript interfaces in fusite-commons:

    Project:

    typescript
    interface Project {
      id: string
      title: string
      slug: string
      status:
        | "planning"
        | "in-progress"
        | "active"
        | "completed"
        | "on-hold"
        | "cancelled"
      technologies: string[]
      technologyColors: { name: string; color?: string }[]
      description?: string
      repositoryUrl?: string
      liveUrl?: string
      featuredImage?: { url: string; alt: string }
      dates: { start?: string; end?: string }
      content?: HtmlContent[] // Notion blocks converted to HTML
    }

    Fuschronicle (Development Logs):

    typescript
    interface Fuschronicle {
      id: string
      title: string
      slug: string
      type:
        | "project-update"
        | "milestone"
        | "learning"
        | "retrospective"
        | "technical"
        | "general"
        | "documentation"
        | "changelog"
      publishedDate?: string
      excerpt?: string
      relatedProject?: Array<{ id: string; title: string; slug: string }>
      content: HtmlContent[]
    }

    HtmlContent (Pre-rendered Notion Blocks):

    typescript
    interface HtmlContent {
      type: "heading_1" | "heading_2" | "paragraph" | "code" | "image" |
            "bulleted_list" | "numbered_list" | ... // 25+ types
      html: string  // Pre-processed HTML
      content?: string
      language?: string  // For code blocks
      annotations?: {
        bold?: boolean
        italic?: boolean
        code?: boolean
        color?: string
      }
      href?: string
      src?: string
    }

    This rich type system ensures:

  • Compile-time validation of data transformations
  • IntelliSense support for API consumers
  • Shared understanding between API and web projects
  • Self-documenting code through expressive types

  • Deployment Architecture

    Serverless Framework Configuration

    yaml
    provider:
      name: aws
      runtime: nodejs22.x
      httpApi:
        id: ${env:API_GATEWAY_ID}
        disableDefaultEndpoint: true # Custom domain only
      logRetentionInDays: 90
    
    logs:
      lambda:
        logFormat: JSON # Structured logging for CloudWatch
    
    functions:
      express-handler:
        handler: src/lambdaHandler.handler
        events:
          - httpApi: /fusite
          - httpApi: /fusite/{proxy+}

    Key Decisions:

  • Single Lambda Function: Express router handles all paths (reduces cold starts)
  • Custom API Gateway: Not using default endpoint (adds this handler to my central api used to all projects under base path /fusite)
  • JSON Logging: Structured logs for production observability
  • 90-Day Retention: Balance between debugging needs and cost
  • Infrastructure-as-Code Benefits

  • Reproducible Deployments: serverless deploy recreates entire stack
  • Multi-Stage Support: Deploy to dev/staging/prod with environment variables
  • Version Control: Infrastructure configuration lives in git
  • Automatic IAM Roles: Serverless Framework handles permissions

  • Key Design Decisions

    1. Notion Over Traditional Database

    Rationale:

  • Content-First Use Case: Projects and chronicles are editorial content, not transactional data
  • Editing UX: Notion's interface beats any custom admin panel
  • Relational Data: Notion supports foreign keys (relations) natively
  • Schema Evolution: Add properties without migrations
  • Collaboration: Multiple editors can manage content simultaneously
  • Trade-offs:

  • API Rate Limits: 3 requests/second (mitigated by caching in web layer)
  • Query Complexity: Limited filtering compared to SQL
  • Vendor Lock-in: Tight coupling to Notion (mitigated by abstraction layer)
  • 2. Shared fusite-commons Library

    Rationale:

  • DRY Principle: API and web both need identical Notion logic
  • Type Safety: Shared types prevent API/client mismatches
  • Single Responsibility: Commons owns Notion integration, consumers own presentation
  • Implementation:

  • Monorepo structure with file:../commons dependency
  • Separate compilation (tsc in commons builds to dist/)
  • Exported services, utilities, and types
  • 3. Serverless Express Instead of Pure Lambda

    Rationale:

  • Familiar Patterns: Express middleware and routing are battle-tested
  • Ecosystem: Access to thousands of Express plugins
  • Developer Experience: Local testing with bun run dev (no AWS emulation)
  • Migration Path: Can move to ECS/Fargate without rewriting
  • Trade-offs:

  • Cold Start Overhead: serverless-http adds ~50ms (acceptable for non-critical path)
  • Lambda Limitations: 6MB response limit, 15-minute timeout (not issues for this API)
  • 4. Read-Only Public API

    Rationale:

  • Simplicity: No authentication, authorization, or state management
  • Security: Minimized attack surface (no write operations)
  • Performance: Aggressive caching possible (no invalidation concerns)
  • Future Extensions:

  • Write operations behind authentication
  • Webhook handlers for Notion updates
  • Task management endpoints

  • Performance Characteristics

    API Response Times

    Typical Metrics (measured from CloudWatch):

  • Health Check: 10-20ms
  • Project List: 200-500ms (Notion API call + transformation)
  • Single Project: 300-800ms (includes content blocks fetch)
  • Cold Start: ~1-2 seconds (Node.js 22 + Express initialization)
  • Optimization Strategies:

  • Parallel Fetching: Multiple Notion API calls use Promise.all()
  • Content Stripping: List endpoints omit heavy content arrays
  • Selective Fetching: Only fetch content blocks when needed
  • Client-Side Caching: Web layer caches responses during build
  • Scaling Characteristics

    Lambda Auto-Scaling:

  • Concurrent Executions: Auto-scales to thousands (AWS account limit)
  • No Configuration: No capacity planning or instance management
  • Cost Model: Pay-per-request (no idle costs)
  • API Gateway:

  • Throttling: 10,000 requests/second default limit
  • Caching: Optional response caching (not enabled, web layer handles this)

  • Technical Highlights

    Architectural Patterns Demonstrated

    PatternImplementationBenefit
    Service LayerNotionService abstracts external APITestable, reusable, maintainable
    Repository PatternControllers delegate to servicesDecoupled from data source
    Response EnvelopeConsistent { success, data, error }Predictable client handling
    Lazy LoadingAWS SDK imported conditionallyReduced bundle size
    Environment SegregationBUILD_ENV-aware logicProduction safety
    Error AdapterNotion errors → user messagesBetter debugging UX

    Technology Proficiencies Showcased

  • Serverless Architecture: AWS Lambda, API Gateway, Serverless Framework
  • TypeScript: Advanced types, strict mode, module abstraction
  • Express: Middleware design, routing, error handling
  • Notion API: Database queries, pagination, rich content extraction
  • AWS Services: S3, CloudFront, Lambda, API Gateway
  • Testing: Bun test runner, module mocking, integration tests
  • Monorepo Management: Shared libraries, workspace dependencies

  • Summary

    Fusite API demonstrates that unconventional data stores can enable elegant architectures. By leveraging Notion as a database, the system achieves:

  • Superior Content Management: Notion's editing UX beats custom admin panels
  • Type Safety: Full TypeScript with shared domain models
  • Serverless Scalability: Auto-scaling Lambda without server management
  • Code Reuse: Shared fusite-commons library eliminates duplication
  • Production-Ready: Structured logging, error handling, multi-stage deployment
  • Extensibility: Clean architecture enables future feature additions
  • The architecture proves that modern APIs don't require traditional databases—thoughtful abstraction layers can turn collaborative tools into first-class data stores while maintaining professional standards for type safety, testing, and deployment.