API Design Principles
Security First
Authentication on every endpoint with organization-scoped data access
RESTful Design
Consistent HTTP methods, status codes, and resource naming conventions
Type Safety
Full TypeScript implementation with runtime validation using Zod
Real-time Support
WebSocket integration for live updates and collaboration features
API Structure
Endpoint Organization
/api/
├── auth/ # Authentication endpoints
│ ├── token/ # JWT token management
│ └── debug/ # Auth debugging (dev only)
├── people/ # Contact management
│ ├── [personId]/ # Individual contact operations
│ ├── search/ # Contact search
│ ├── bulk/ # Bulk operations
│ └── suggest/ # Auto-suggestions
├── deals/ # Deal management
│ ├── [id]/ # Individual deal operations
│ ├── pipeline/ # Pipeline management
│ └── favorites/ # Favorited deals
├── properties/ # Property management
│ ├── [id]/ # Individual property operations
│ ├── search/ # Property search
│ └── export/ # Data export
├── communications/ # Communication hub
│ ├── manual/ # Manual communications
│ └── sync/ # Communication sync
├── twilio/ # Phone system
│ ├── calls/ # Call management
│ ├── sms/ # SMS messaging
│ └── recordings/ # Call recordings
├── nylas/ # Email/calendar
│ ├── emails/ # Email management
│ ├── calendars/ # Calendar integration
│ └── events/ # Event management
├── ai/ # AI services
│ ├── sentiment/ # Sentiment analysis
│ ├── voice-search/ # Voice commands
│ └── insights/ # AI insights
└── webhooks/ # External webhooks
├── twilio/ # Twilio webhooks
├── nylas/ # Nylas webhooks
└── stripe/ # Stripe webhooks
Authentication & Authorization
Request Authentication
Every API request requires authentication and organization context:// Standard API route structure
export async function POST(req: Request) {
try {
// 1. Authentication & organization context
const { userId, orgId } = await auth();
if (!userId || !orgId) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// 2. Input validation
const body = await req.json();
const validatedData = createPersonSchema.parse(body);
// 3. Permission check
await requirePermission(userId, orgId, Permission.CONTACTS_CREATE);
// 4. Business logic with organization isolation
const person = await database.person.create({
data: {
...validatedData,
organizationId: orgId, // Enforce organization isolation
createdBy: userId
}
});
// 5. Structured response with CORS
const headers = new Headers();
addCorsHeaders(headers, req.headers.get('origin'));
return NextResponse.json(person, { headers });
} catch (error) {
return handleApiError(error, req);
}
}
Middleware Stack
// apps/api/middleware.ts
import { auth } from '@repo/auth/server';
import { rateLimit } from '@/lib/rate-limit';
import { addCorsHeaders } from '@/lib/cors';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Skip middleware for public routes
if (isPublicRoute(pathname)) {
return NextResponse.next();
}
// Rate limiting
const rateLimitResult = await rateLimit(request);
if (!rateLimitResult.success) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{ status: 429 }
);
}
// Authentication
try {
const { userId, orgId } = await auth();
if (!userId) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Add context to headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', userId);
if (orgId) {
requestHeaders.set('x-organization-id', orgId);
}
// CORS headers
const response = NextResponse.next({
request: { headers: requestHeaders }
});
addCorsHeaders(response.headers, request.headers.get('origin'));
return response;
} catch (error) {
log.error('Middleware authentication failed', {
error: parseError(error),
path: pathname
});
return NextResponse.json(
{ error: 'Authentication failed' },
{ status: 401 }
);
}
}
export const config = {
matcher: [
'/api/((?!health|webhooks/stripe).*)'
]
};
Input Validation & Type Safety
Zod Schema Validation
import { z } from 'zod';
// Person creation schema
export const createPersonSchema = z.object({
firstName: z.string().min(1).max(50),
lastName: z.string().min(1).max(50),
email: z.string().email().optional(),
phone: z.string().regex(/^\+?[1-9]\d{1,14}$/).optional(),
address: z.string().max(200).optional(),
city: z.string().max(50).optional(),
state: z.string().max(50).optional(),
zipCode: z.string().max(10).optional(),
source: z.enum(['WEBSITE', 'REFERRAL', 'SOCIAL_MEDIA', 'COLD_CALL']).optional(),
tags: z.array(z.string()).default([]),
customFields: z.record(z.any()).optional()
});
// Deal creation schema
export const createDealSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().max(1000).optional(),
dealType: z.enum(['LISTING', 'BUYER_REPRESENTATION', 'RENTAL']),
amount: z.number().positive().optional(),
commission: z.number().positive().optional(),
commissionRate: z.number().min(0).max(100).optional(),
expectedCloseDate: z.string().datetime().optional(),
personId: z.string().cuid().optional(),
propertyId: z.string().cuid().optional(),
stage: z.string().min(1),
probability: z.number().min(0).max(100).default(50),
tags: z.array(z.string()).default([])
});
// Property search schema
export const propertySearchSchema = z.object({
query: z.string().optional(),
propertyType: z.enum(['SINGLE_FAMILY', 'CONDO', 'TOWNHOUSE']).optional(),
minPrice: z.number().positive().optional(),
maxPrice: z.number().positive().optional(),
bedrooms: z.number().int().min(0).optional(),
bathrooms: z.number().min(0).optional(),
city: z.string().optional(),
state: z.string().optional(),
zipCode: z.string().optional(),
status: z.enum(['ACTIVE', 'PENDING', 'SOLD']).optional(),
page: z.number().int().min(1).default(1),
limit: z.number().int().min(1).max(100).default(20)
});
Response Type Definitions
// Standard API response types
export interface ApiResponse<T = unknown> {
data?: T;
error?: string;
message?: string;
pagination?: PaginationMeta;
}
export interface PaginationMeta {
page: number;
limit: number;
total: number;
totalPages: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
}
// Specific response types
export interface PersonResponse {
id: string;
firstName: string;
lastName: string;
email?: string;
phone?: string;
address?: string;
city?: string;
state?: string;
zipCode?: string;
source?: LeadSource;
status: PersonStatus;
leadScore?: number;
lastContactAt?: string;
tags: string[];
createdAt: string;
updatedAt: string;
_count?: {
deals: number;
communications: number;
tasks: number;
};
}
export interface DealResponse {
id: string;
title: string;
description?: string;
dealType: DealType;
amount?: number;
commission?: number;
commissionRate?: number;
status: DealStatus;
stage: string;
probability?: number;
expectedCloseDate?: string;
actualCloseDate?: string;
person?: PersonResponse;
property?: PropertyResponse;
tags: string[];
createdAt: string;
updatedAt: string;
}
Error Handling
Centralized Error Handler
// lib/api-error-handler.ts
export class ApiError extends Error {
constructor(
public statusCode: number,
message: string,
public code?: string
) {
super(message);
this.name = 'ApiError';
}
}
export class ValidationError extends ApiError {
constructor(message: string, public issues?: unknown[]) {
super(400, message, 'VALIDATION_ERROR');
}
}
export class PermissionError extends ApiError {
constructor(message: string = 'Insufficient permissions') {
super(403, message, 'PERMISSION_DENIED');
}
}
export class NotFoundError extends ApiError {
constructor(resource: string = 'Resource') {
super(404, `${resource} not found`, 'NOT_FOUND');
}
}
export function handleApiError(error: unknown, req: Request): NextResponse {
log.error('API Error', {
error: parseError(error),
url: req.url,
method: req.method
});
// Zod validation errors
if (error instanceof z.ZodError) {
return NextResponse.json({
error: 'Validation failed',
issues: error.issues
}, { status: 400 });
}
// Custom API errors
if (error instanceof ApiError) {
return NextResponse.json({
error: error.message,
code: error.code
}, { status: error.statusCode });
}
// Prisma errors
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
return NextResponse.json({
error: 'A record with this information already exists',
code: 'DUPLICATE_RECORD'
}, { status: 409 });
}
if (error.code === 'P2025') {
return NextResponse.json({
error: 'Record not found',
code: 'NOT_FOUND'
}, { status: 404 });
}
}
// Generic server error
return NextResponse.json({
error: 'Internal server error',
code: 'INTERNAL_ERROR'
}, { status: 500 });
}
Rate Limiting
Postgres Quota Buckets + In-Memory Guardrails
// lib/rate-limit.ts
import { createRateLimiter, slidingWindow } from '@repo/scheduling';
// @repo/scheduling stores distributed quota buckets in Postgres.
// This lightweight map still guards each API instance from local bursts.
type RateLimitOptions = {
interval: number;
uniqueTokenPerInterval: number;
limit: number;
};
type CacheEntry = {
timestamps: number[];
expiresAt: number;
};
const MAX_CACHE_SIZE = 5000;
const rateLimitCache = new Map<string, CacheEntry>();
const apiLimiter = createRateLimiter({
prefix: 'api',
limiter: slidingWindow(100, '60s'),
});
export async function rateLimit(
request: NextRequest,
options: RateLimitOptions = {
interval: 60 * 1000,
uniqueTokenPerInterval: 500,
limit: 100,
}
): Promise<{ success: boolean; limit: number; remaining: number; reset: number }> {
const ip = getClientIP(request);
const userId = request.headers.get('x-user-id');
const identifier = userId ? `user:${userId}` : `ip:${ip}`;
const now = Date.now();
const cached = rateLimitCache.get(identifier);
const timestamps =
cached && cached.expiresAt > now ? [...cached.timestamps] : [];
const windowStart = now - options.interval;
const recentRequests = timestamps.filter((timestamp) => timestamp > windowStart);
if (recentRequests.length >= options.limit) {
const oldestRequest = Math.min(...recentRequests);
const resetTime = oldestRequest + options.interval;
return {
success: false,
limit: options.limit,
remaining: 0,
reset: resetTime,
};
}
recentRequests.push(now);
if (rateLimitCache.size >= MAX_CACHE_SIZE) {
const oldestKey = rateLimitCache.keys().next().value;
if (oldestKey) {
rateLimitCache.delete(oldestKey);
}
}
rateLimitCache.set(identifier, {
timestamps: recentRequests,
expiresAt: now + options.interval,
});
const distributed = await apiLimiter.limit(identifier);
if (!distributed.success) {
return distributed;
}
return {
success: true,
limit: options.limit,
remaining: options.limit - recentRequests.length,
reset: now + options.interval,
};
}
export const rateLimits = {
auth: { interval: 15 * 60 * 1000, limit: 5 },
api: { interval: 60 * 1000, limit: 100 },
webhook: { interval: 60 * 1000, limit: 1000 },
upload: { interval: 60 * 1000, limit: 10 },
};
CORS Configuration
Cross-Origin Resource Sharing
// lib/cors.ts
export function addCorsHeaders(
headers: Headers,
origin?: string | null
): void {
// Allow requests from app domains
const allowedOrigins = [
'http://localhost:3000', // Local development
'http://localhost:3001', // Local development (alt port)
'https://app.winnerr.com', // Production app
'https://staging.winnerr.com', // Staging app
'chrome-extension://*' // Chrome extension
];
// Check if origin is allowed
const isAllowed = origin && (
allowedOrigins.includes(origin) ||
origin.startsWith('chrome-extension://') ||
(process.env.NODE_ENV === 'development' && origin.includes('localhost'))
);
if (isAllowed) {
headers.set('Access-Control-Allow-Origin', origin);
headers.set('Access-Control-Allow-Credentials', 'true');
}
headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
headers.set('Access-Control-Allow-Headers',
'Content-Type, Authorization, x-organization-id, x-user-id');
headers.set('Access-Control-Max-Age', '86400'); // 24 hours
}
// Handle preflight requests
export async function handlePreflight(request: Request): Promise<Response> {
const headers = new Headers();
addCorsHeaders(headers, request.headers.get('origin'));
return new Response(null, {
status: 200,
headers
});
}
Real-time Communication
WebSocket Integration
// lib/socket-io.ts
import { Server as SocketIOServer } from 'socket.io';
import { auth } from '@repo/auth/server';
export function initializeSocketIO(server: unknown) {
const io = new SocketIOServer(server, {
cors: {
origin: process.env.NEXT_PUBLIC_APP_URL,
credentials: true
}
});
// Authentication middleware
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token;
const { userId, orgId } = await verifyToken(token);
if (!userId || !orgId) {
return next(new Error('Authentication failed'));
}
socket.userId = userId;
socket.orgId = orgId;
// Join organization room
socket.join(`org:${orgId}`);
next();
} catch (error) {
next(new Error('Authentication failed'));
}
});
io.on('connection', (socket) => {
log.info('Socket connected', {
userId: socket.userId,
orgId: socket.orgId
});
// Handle presence updates
socket.on('presence:join', (data) => {
socket.to(`org:${socket.orgId}`).emit('presence:user_joined', {
userId: socket.userId,
...data
});
});
socket.on('presence:leave', () => {
socket.to(`org:${socket.orgId}`).emit('presence:user_left', {
userId: socket.userId
});
});
// Handle real-time data updates
socket.on('deal:update', async (data) => {
try {
// Validate permissions
await requirePermission(socket.userId, socket.orgId, Permission.DEALS_EDIT);
// Update deal
const deal = await updateDeal(data.dealId, data.updates, socket.orgId);
// Broadcast to organization members
socket.to(`org:${socket.orgId}`).emit('deal:updated', {
dealId: data.dealId,
updates: data.updates,
updatedBy: socket.userId
});
} catch (error) {
socket.emit('error', { message: 'Failed to update deal' });
}
});
socket.on('disconnect', () => {
socket.to(`org:${socket.orgId}`).emit('presence:user_left', {
userId: socket.userId
});
});
});
return io;
}
Real-time Event Broadcasting
// lib/realtime-events.ts
export class RealtimeEventBus {
constructor(private io: SocketIOServer) {}
// Broadcast to specific organization
emitToOrganization(orgId: string, event: string, data: unknown) {
this.io.to(`org:${orgId}`).emit(event, {
...data,
timestamp: new Date().toISOString()
});
}
// Broadcast deal updates
broadcastDealUpdate(orgId: string, dealId: string, updates: unknown, userId: string) {
this.emitToOrganization(orgId, 'deal:updated', {
dealId,
updates,
updatedBy: userId
});
}
// Broadcast new communications
broadcastCommunication(orgId: string, communication: unknown) {
this.emitToOrganization(orgId, 'communication:new', {
communication
});
}
// Broadcast presence updates
broadcastPresence(orgId: string, userId: string, status: 'online' | 'offline') {
this.emitToOrganization(orgId, 'presence:update', {
userId,
status,
timestamp: new Date().toISOString()
});
}
}
// Usage in API routes
export async function POST(req: Request) {
// ... handle deal creation ...
// Broadcast real-time update
realtimeEvents.broadcastDealUpdate(
orgId,
deal.id,
{ status: 'created' },
userId
);
return NextResponse.json(deal);
}
API Documentation
OpenAPI Specification
// lib/openapi-spec.ts
export const openApiSpec = {
openapi: '3.0.0',
info: {
title: 'Winnerr CRM API',
version: '1.0.0',
description: 'Comprehensive API for real estate CRM operations'
},
servers: [
{
url: 'https://api.winnerr.com',
description: 'Production server'
},
{
url: 'http://localhost:3002',
description: 'Development server'
}
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
},
schemas: {
Person: {
type: 'object',
properties: {
id: { type: 'string' },
firstName: { type: 'string' },
lastName: { type: 'string' },
email: { type: 'string', format: 'email' },
phone: { type: 'string' },
status: {
type: 'string',
enum: ['PROSPECT', 'LEAD', 'QUALIFIED', 'ACTIVE_CLIENT', 'PAST_CLIENT']
},
leadScore: { type: 'number', minimum: 0, maximum: 100 },
tags: { type: 'array', items: { type: 'string' } },
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' }
}
}
}
},
security: [{ bearerAuth: [] }]
};
Performance Optimization
Data Access Strategy
// lib/deals-service.ts
import { database } from '@repo/database/server';
import { parseError } from '@repo/observability/error';
export async function listActiveDeals(orgId: string) {
try {
return await database.deal.findMany({
where: {
organizationId: orgId,
status: 'OPEN',
},
include: { person: true, property: true },
orderBy: { updatedAt: 'desc' },
take: 100,
});
} catch (error) {
log.error('Failed to list active deals', { error: parseError(error) });
throw error;
}
}
// Usage in API routes
export async function GET(req: Request) {
const { orgId } = await auth();
const deals = await listActiveDeals(orgId);
return NextResponse.json(deals);
}
Next Steps
Real-time Features
Deep dive into WebSocket and collaboration features
Integration Patterns
Learn about third-party service integrations
Performance Optimization
API optimization and scaling strategies
This API architecture provides a solid foundation for real estate CRM operations while maintaining security, performance, and developer experience.