REST API Architecture
Architecture Diagram
The following diagram illustrates how REST API calls flow through the monorepo architecture, from Next.js applications through the centralized fetch layer to external services.
Key Components
- Applications Layer (Blue): Next.js applications that consume REST APIs
- Fetch Package (Green): Centralized API client functions for each service
- Schema Package (Orange): Zod validation and TypeScript type definitions
- External Services (Purple): Third-party APIs and internal services
- Response Flow (Pink): Data transformation pipeline from raw response to UI
This document explains how the monorepo handles REST API calls, focusing on the architecture, patterns, and best practices implemented across the Next.js applications.
Overview
The monorepo uses a centralized REST API architecture through the @schwab/fetch package, which provides typed, validated, and consistent API interactions across all applications. The system primarily integrates with:
- Drupal CMS via JSON:API
- LaunchDarkly for feature flags
- Analytics services for tracking
- Internal APIs for various business logic
API Architecture Components
1. Fetch Package (@schwab/fetch)
The central API layer that handles all external REST API communications:
// packages/fetch/src/index.ts
export { getStory } from './drupal/cmap-api/getStory';
export { getRelatedContent } from './drupal/cmap-api/getRelatedContent';
export { getProgrammaticCta } from './drupal/cmap-api/getProgrammaticCta';
export { drupalFetch } from './drupal/drupalFetch';
2. Core Fetch Function
The base drupalFetch function provides standardized REST API calls:
// Simplified example based on codebase patterns
export async function drupalFetch<T>(
source: EDrupalSource,
endpoint: string,
options?: RequestInit
): Promise<ApiResponse<T>> {
const baseUrl = getBaseUrl(source);
const url = `${baseUrl}${endpoint}`;
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...options?.headers,
},
...options,
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return { data, isOk: true };
} catch (error) {
return { data: null, isOk: false, error };
}
}
API Integration Patterns
1. Story Content API
Fetching story/article content from Drupal CMS:
// packages/fetch/src/drupal/cmap-api/getStory.ts
export async function getStory(
source: EDrupalSource,
urlAlias: string
): Promise<ApiResponse<TStory>> {
// Fetch raw data from Drupal JSON:API
const response = await drupalFetch(source, `/api/story${urlAlias}`);
if (!response.isOk) {
return response;
}
// Validate with Zod schema
const validatedData = FetchedStorySchema.parse(response.data);
// Transform to UI schema
const transformedStory = transformStory(validatedData);
return {
data: transformedStory,
isOk: true
};
}
2. Parallel API Loading
Multiple APIs loaded concurrently for performance:
// Used in StoryLoader component
const [story, relatedContent, programmaticCta] = await Promise.all([
getStory(EDrupalSource.Education, urlAlias),
getRelatedContent(EDrupalSource.Education, urlAlias),
getProgrammaticCta(EDrupalSource.Education, urlAlias),
]);
3. Feature Flag Integration
REST API calls to LaunchDarkly for dynamic configuration:
// Feature flag evaluation with API fallback
export async function evaluateStaticFlag<T>(
allFlags: TFeatureFlag[],
code: string,
flagKey: string
): Promise<T | undefined> {
// Try local flags first
const localFlag = allFlags.find(flag =>
flag.key === flagKey && flag.code === code
);
if (localFlag) {
return localFlag.value;
}
// Fallback to API call
return await fetchFeatureFlagFromAPI(flagKey, code);
}
API Response Handling
1. Standardized Response Type
All API functions return a consistent response format:
type ApiResponse<T> = {
data: T | null;
isOk: boolean;
error?: Error;
status?: number;
headers?: Headers;
};
2. Error Handling Patterns
Comprehensive error handling at multiple levels:
// Component-level error handling
export default async function PageComponent({ params }) {
const story = await getStory(EDrupalSource.Education, urlAlias);
if (!story?.isOk) {
notFound(); // Next.js 404 handling
}
// Additional validation
if (!story.data?.heading) {
throw new Error('Invalid story data');
}
return <StoryComponent story={story.data} />;
}
3. Validation Pipeline
Each API response goes through validation:
// 1. Raw API response
const rawResponse = await fetch('/api/endpoint');
// 2. Parse JSON
const jsonData = await rawResponse.json();
// 3. Validate with Zod schema
const validatedData = FetchedStorySchema.parse(jsonData);
// 4. Transform for UI consumption
const transformedData = transformStory(validatedData);
// 5. Final validation
const finalData = StorySchema.parse(transformedData);
Environment Configuration
1. API Base URLs
Different environments use different API endpoints:
// Environment-based API configuration
export function getBaseUrl(source: EDrupalSource): string {
const config = {
[EDrupalSource.Education]: {
development: 'https://dev-api.schwab.com',
staging: 'https://staging-api.schwab.com',
production: 'https://api.schwab.com',
},
};
return config[source][process.env.NODE_ENV] || config[source].development;
}
2. Authentication Headers
API calls include appropriate authentication:
// Authentication configuration
const getAuthHeaders = () => ({
'Authorization': `Bearer ${process.env.API_TOKEN}`,
'X-API-Key': process.env.API_KEY,
'X-Client-Version': process.env.APP_VERSION,
});
Caching Strategy
1. Server-Side Caching
Next.js App Router provides built-in caching for API calls:
// Automatic caching with Next.js fetch
export async function getStory(source: EDrupalSource, urlAlias: string) {
const response = await fetch(`${baseUrl}/api/story${urlAlias}`, {
next: { revalidate: 3600 }, // Cache for 1 hour
});
return response.json();
}
2. Static Generation
Pages with API data can be statically generated:
// Static generation with API data
export const dynamic = 'force-static';
export async function generateStaticParams() {
const stories = await getAllStories();
return stories.map(story => ({
alias: story.urlAlias.split('/').filter(Boolean),
}));
}
API Documentation Integration
1. TypeScript Integration
Full TypeScript support with generated types:
// Generated from API schema
export interface DrugalStoryResponse {
uuid: string;
title: string;
body: {
value: string;
format: string;
};
field_components: ComponentData[];
}
2. Schema Validation
Zod schemas provide runtime validation:
export const FetchedStorySchema = z.object({
uuid: z.string().uuid(),
title: z.string().min(1),
subtitle: z.string().optional(),
summary: z.string(),
components: z.array(z.unknown()),
urlAlias: z.string().startsWith('/'),
language: z.string().length(2),
lastPublished: z.string().datetime(),
});
Performance Optimizations
1. Request Deduplication
Multiple components requesting the same data are deduplicated:
// Next.js automatically deduplicates identical requests
const story1 = await getStory(source, urlAlias); // API call
const story2 = await getStory(source, urlAlias); // Uses cached result
2. Streaming Responses
Large API responses can be streamed:
// Streaming API responses for large datasets
export async function streamLargeDataset(endpoint: string) {
const response = await fetch(endpoint);
const reader = response.body?.getReader();
return new ReadableStream({
start(controller) {
function pump() {
return reader?.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
controller.enqueue(value);
return pump();
});
}
return pump();
}
});
}
Testing API Integrations
1. Mock API Responses
API calls are mocked in tests:
// Jest mock for API testing
jest.mock('@schwab/fetch/getStory', () => ({
getStory: jest.fn().mockResolvedValue({
data: mockStoryData,
isOk: true,
}),
}));
2. Integration Testing
End-to-end testing of API integrations:
// Test actual API integration
describe('API Integration', () => {
it('should fetch and transform story data', async () => {
const story = await getStory(EDrupalSource.Education, '/test-story');
expect(story.isOk).toBe(true);
expect(story.data?.heading).toBeDefined();
expect(story.data?.components).toBeInstanceOf(Array);
});
});
Security Considerations
1. Input Sanitization
All API inputs are validated and sanitized:
// URL sanitization
export function sanitizeUrlAlias(alias: string): string {
return alias
.replace(/[^a-zA-Z0-9-/_]/g, '')
.replace(/\/+/g, '/')
.toLowerCase();
}
2. Rate Limiting
API calls include rate limiting protection:
// Simple rate limiting
const rateLimiter = new Map();
export async function rateLimit(key: string, limit: number = 100) {
const now = Date.now();
const windowStart = now - 60000; // 1 minute window
const requests = rateLimiter.get(key) || [];
const validRequests = requests.filter(time => time > windowStart);
if (validRequests.length >= limit) {
throw new Error('Rate limit exceeded');
}
validRequests.push(now);
rateLimiter.set(key, validRequests);
}
Monitoring and Logging
1. API Call Logging
All API calls are logged for monitoring:
// Structured logging for API calls
export async function loggedFetch(url: string, options?: RequestInit) {
const startTime = Date.now();
try {
const response = await fetch(url, options);
const duration = Date.now() - startTime;
console.log({
type: 'api_call',
url,
status: response.status,
duration,
success: response.ok,
});
return response;
} catch (error) {
console.error({
type: 'api_error',
url,
error: error.message,
duration: Date.now() - startTime,
});
throw error;
}
}
2. Error Tracking
API errors are tracked and reported:
// Error tracking integration
export function trackApiError(error: Error, context: any) {
// Send to error tracking service
errorTracker.captureException(error, {
tags: { component: 'api_client' },
extra: context,
});
}
Best Practices
1. Consistent Error Handling
- Always return standardized
ApiResponse<T>types - Handle network errors gracefully
- Provide meaningful error messages to users
2. Type Safety
- Use Zod schemas for runtime validation
- Generate TypeScript types from API schemas
- Validate both request and response data
3. Performance
- Use Next.js caching features appropriately
- Implement request deduplication
- Load data in parallel when possible
4. Security
- Sanitize all inputs
- Use environment variables for sensitive data
- Implement rate limiting where appropriate