Fusite API Low-Level Design
Design DocumentsFusite 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:
- Shared Notion Access: Uses the fusite-commons package for Notion SDK access
- RESTful Endpoints: Provides structured data access t####
fuschronicleController.ts
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:
// 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
// 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)
// 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
// 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
// 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
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
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
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
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
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
# 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
:
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
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
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
API Endpoint Testing
- Request/response validation
- Authentication and authorization
- Error handling
Notion Data Access
- Mock Notion API responses
- Data transformation accuracy
- Error handling for API failures
Content Transformation
- Notion blocks to API format conversion
- Image processing and URL rewriting
- Plain text extraction
Webhook Processing
- Signature validation
- Content change logging
- Error recovery
10. Performance Considerations
9.1 Optimization Strategies
Database Optimization
- Notion API rate limiting compliance
- Connection pooling
- Batch operations where possible
- Efficient query filtering
API Performance
- Response compression
- Selective field loading
- Async processing for heavy operations
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.