Skip to main content
What you’ll get out of this: Master API development with DeelRx CRM’s Next.js API routes, including RESTful design, authentication, validation, error handling, and performance optimization.

API Architecture

Technology Stack

Next.js API Routes

  • File-based routing
  • Built-in middleware
  • TypeScript support
  • Edge runtime support

Authentication

  • Clerk integration
  • JWT tokens
  • Role-based access
  • Multi-tenant security

API Design Principles

RESTful Design

  • Use HTTP methods appropriately (GET, POST, PUT, DELETE)
  • Implement consistent URL patterns
  • Return appropriate HTTP status codes
  • Use proper content types and headers
  • Follow RESTful resource naming conventions

Multi-Tenancy

All API endpoints automatically scope data by team/organization to ensure data isolation and security.
  • Team Scoping: All operations include teamId validation
  • Data Isolation: Complete separation between organizations
  • Security: Row-level security and access controls
  • Performance: Optimized for multi-tenant queries

API Route Structure

File-Based Routing

apps/app/app/api/
├── customers/
│   ├── route.ts              # GET /api/customers, POST /api/customers
│   └── [id]/
│       ├── route.ts          # GET /api/customers/[id], PUT /api/customers/[id], DELETE /api/customers/[id]
│       └── orders/
│           └── route.ts      # GET /api/customers/[id]/orders
├── orders/
│   ├── route.ts              # GET /api/orders, POST /api/orders
│   └── [id]/
│       ├── route.ts          # GET /api/orders/[id], PUT /api/orders/[id], DELETE /api/orders/[id]
│       └── items/
│           └── route.ts      # GET /api/orders/[id]/items, POST /api/orders/[id]/items
├── products/
│   ├── route.ts              # GET /api/products, POST /api/products
│   └── [id]/
│       └── route.ts          # GET /api/products/[id], PUT /api/products/[id], DELETE /api/products/[id]
└── health/
    └── route.ts              # GET /api/health

Route Handler Structure

// apps/app/app/api/customers/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs';
import { z } from 'zod';
import { database } from '@repo/database';

// GET /api/customers
export async function GET(request: NextRequest) {
  try {
    // Authentication
    const { userId, orgId } = auth();
    if (!userId || !orgId) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    // Query parameters
    const { searchParams } = new URL(request.url);
    const page = parseInt(searchParams.get('page') || '1');
    const limit = parseInt(searchParams.get('limit') || '10');
    const search = searchParams.get('search') || '';

    // Database query
    const customers = await database.customer.findMany({
      where: {
        teamId: orgId,
        ...(search && {
          OR: [
            { firstName: { contains: search, mode: 'insensitive' } },
            { lastName: { contains: search, mode: 'insensitive' } },
            { email: { contains: search, mode: 'insensitive' } }
          ]
        })
      },
      orderBy: { createdAt: 'desc' },
      skip: (page - 1) * limit,
      take: limit
    });

    const total = await database.customer.count({
      where: {
        teamId: orgId,
        ...(search && {
          OR: [
            { firstName: { contains: search, mode: 'insensitive' } },
            { lastName: { contains: search, mode: 'insensitive' } },
            { email: { contains: search, mode: 'insensitive' } }
          ]
        })
      }
    });

    return NextResponse.json({
      data: customers,
      pagination: {
        page,
        limit,
        total,
        pages: Math.ceil(total / limit)
      }
    });
  } catch (error) {
    console.error('Error fetching customers:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

// POST /api/customers
export async function POST(request: NextRequest) {
  try {
    // Authentication
    const { userId, orgId } = auth();
    if (!userId || !orgId) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    // Request validation
    const body = await request.json();
    const customerSchema = z.object({
      email: z.string().email(),
      firstName: z.string().min(1).max(50),
      lastName: z.string().min(1).max(50),
      phone: z.string().optional(),
      address: z.object({
        street: z.string().optional(),
        city: z.string().optional(),
        state: z.string().optional(),
        zipCode: z.string().optional(),
        country: z.string().optional()
      }).optional()
    });

    const validatedData = customerSchema.parse(body);

    // Check for existing customer
    const existingCustomer = await database.customer.findUnique({
      where: {
        email: validatedData.email,
        teamId: orgId
      }
    });

    if (existingCustomer) {
      return NextResponse.json(
        { error: 'Customer with this email already exists' },
        { status: 409 }
      );
    }

    // Create customer
    const customer = await database.customer.create({
      data: {
        ...validatedData,
        teamId: orgId
      }
    });

    return NextResponse.json({ data: customer }, { status: 201 });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Validation error', details: error.errors },
        { status: 400 }
      );
    }

    console.error('Error creating customer:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Authentication and Authorization

Clerk Integration

Authentication Middleware

// lib/auth.ts
import { auth } from '@clerk/nextjs';
import { NextRequest } from 'next/server';

export async function requireAuth(request: NextRequest) {
  const { userId, orgId } = auth();
  
  if (!userId) {
    throw new Error('Authentication required');
  }
  
  if (!orgId) {
    throw new Error('Organization membership required');
  }
  
  return { userId, orgId };
}

export async function requireRole(request: NextRequest, allowedRoles: string[]) {
  const { userId, orgId } = await requireAuth(request);
  
  // Check user role in organization
  const userMembership = await database.teamMembership.findFirst({
    where: {
      userId,
      teamId: orgId
    },
    include: {
      role: true
    }
  });
  
  if (!userMembership || !allowedRoles.includes(userMembership.role.name)) {
    throw new Error('Insufficient permissions');
  }
  
  return { userId, orgId, role: userMembership.role.name };
}

Role-Based Access Control

// API route with role-based access
export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    // Require admin role for deletion
    const { userId, orgId } = await requireRole(request, ['admin', 'owner']);
    
    const { id } = await params;
    
    // Soft delete customer
    const customer = await database.customer.update({
      where: {
        id,
        teamId: orgId
      },
      data: {
        deletedAt: new Date()
      }
    });
    
    return NextResponse.json({ data: customer });
  } catch (error) {
    if (error.message === 'Authentication required') {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }
    
    if (error.message === 'Insufficient permissions') {
      return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
    }
    
    console.error('Error deleting customer:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Request Validation

Schema Validation with Zod

// lib/validation/customer.ts
import { z } from 'zod';

export const customerCreateSchema = z.object({
  email: z.string().email('Invalid email address'),
  firstName: z.string()
    .min(1, 'First name is required')
    .max(50, 'First name must be less than 50 characters'),
  lastName: z.string()
    .min(1, 'Last name is required')
    .max(50, 'Last name must be less than 50 characters'),
  phone: z.string()
    .regex(/^\+?[\d\s\-\(\)]+$/, 'Invalid phone number format')
    .optional(),
  address: z.object({
    street: z.string().optional(),
    city: z.string().optional(),
    state: z.string().optional(),
    zipCode: z.string().optional(),
    country: z.string().optional()
  }).optional()
});

export const customerUpdateSchema = customerCreateSchema.partial();

export const customerQuerySchema = z.object({
  page: z.string().regex(/^\d+$/).transform(Number).default('1'),
  limit: z.string().regex(/^\d+$/).transform(Number).default('10'),
  search: z.string().optional(),
  sortBy: z.enum(['firstName', 'lastName', 'email', 'createdAt']).default('createdAt'),
  sortOrder: z.enum(['asc', 'desc']).default('desc')
});

Validation Middleware

// lib/validation/middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

export function validateRequest<T>(schema: z.ZodSchema<T>) {
  return async (request: NextRequest): Promise<T> => {
    try {
      const body = await request.json();
      return schema.parse(body);
    } catch (error) {
      if (error instanceof z.ZodError) {
        throw new ValidationError('Invalid request data', error.errors);
      }
      throw error;
    }
  };
}

export class ValidationError extends Error {
  constructor(message: string, public details: z.ZodIssue[]) {
    super(message);
    this.name = 'ValidationError';
  }
}

Error Handling

Centralized Error Handling

// lib/errors.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 details: any) {
    super(400, message, 'VALIDATION_ERROR');
  }
}

export class NotFoundError extends APIError {
  constructor(resource: string) {
    super(404, `${resource} not found`, 'NOT_FOUND');
  }
}

export class UnauthorizedError extends APIError {
  constructor() {
    super(401, 'Unauthorized', 'UNAUTHORIZED');
  }
}

export class ForbiddenError extends APIError {
  constructor() {
    super(403, 'Forbidden', 'FORBIDDEN');
  }
}

Error Response Format

// lib/error-handler.ts
import { NextResponse } from 'next/server';
import { APIError } from './errors';

export function handleAPIError(error: unknown) {
  console.error('API Error:', error);
  
  if (error instanceof APIError) {
    return NextResponse.json(
      {
        error: error.message,
        code: error.code,
        statusCode: error.statusCode
      },
      { status: error.statusCode }
    );
  }
  
  if (error instanceof z.ZodError) {
    return NextResponse.json(
      {
        error: 'Validation error',
        code: 'VALIDATION_ERROR',
        details: error.errors
      },
      { status: 400 }
    );
  }
  
  return NextResponse.json(
    {
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    },
    { status: 500 }
  );
}

Response Formatting

Standard Response Format

// lib/response.ts
import { NextResponse } from 'next/server';

export interface APIResponse<T = any> {
  data?: T;
  error?: string;
  code?: string;
  pagination?: {
    page: number;
    limit: number;
    total: number;
    pages: number;
  };
  meta?: {
    timestamp: string;
    requestId: string;
  };
}

export function successResponse<T>(
  data: T,
  status: number = 200,
  pagination?: APIResponse['pagination']
): NextResponse<APIResponse<T>> {
  return NextResponse.json({
    data,
    pagination,
    meta: {
      timestamp: new Date().toISOString(),
      requestId: crypto.randomUUID()
    }
  }, { status });
}

export function errorResponse(
  error: string,
  status: number = 500,
  code?: string
): NextResponse<APIResponse> {
  return NextResponse.json({
    error,
    code,
    meta: {
      timestamp: new Date().toISOString(),
      requestId: crypto.randomUUID()
    }
  }, { status });
}

Pagination

// lib/pagination.ts
export interface PaginationOptions {
  page: number;
  limit: number;
  total: number;
}

export function createPaginationResponse(options: PaginationOptions) {
  const { page, limit, total } = options;
  const pages = Math.ceil(total / limit);
  
  return {
    page,
    limit,
    total,
    pages,
    hasNext: page < pages,
    hasPrev: page > 1
  };
}

// Usage in API route
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get('page') || '1');
  const limit = parseInt(searchParams.get('limit') || '10');
  
  const [items, total] = await Promise.all([
    database.customer.findMany({
      where: { teamId },
      skip: (page - 1) * limit,
      take: limit
    }),
    database.customer.count({ where: { teamId } })
  ]);
  
  return successResponse(items, 200, createPaginationResponse({
    page,
    limit,
    total
  }));
}

Performance Optimization

Caching Strategy

// lib/cache.ts
import { Redis } from 'ioredis';

const redis = new Redis(process.env.REDIS_URL!);

export async function getCachedData<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttl: number = 300
): Promise<T> {
  try {
    const cached = await redis.get(key);
    if (cached) {
      return JSON.parse(cached);
    }
    
    const data = await fetcher();
    await redis.setex(key, ttl, JSON.stringify(data));
    return data;
  } catch (error) {
    console.error('Cache error:', error);
    return await fetcher();
  }
}

// Usage in API route
export async function GET(request: NextRequest) {
  const { orgId } = await requireAuth(request);
  const cacheKey = `customers:${orgId}`;
  
  const customers = await getCachedData(
    cacheKey,
    () => database.customer.findMany({
      where: { teamId: orgId }
    }),
    300 // 5 minutes
  );
  
  return successResponse(customers);
}

Database Query Optimization

// Optimized query with proper indexing
export async function GET(request: NextRequest) {
  const { orgId } = await requireAuth(request);
  const { searchParams } = new URL(request.url);
  
  const where = {
    teamId: orgId,
    deletedAt: null
  };
  
  // Add search filter if provided
  const search = searchParams.get('search');
  if (search) {
    where.OR = [
      { firstName: { contains: search, mode: 'insensitive' } },
      { lastName: { contains: search, mode: 'insensitive' } },
      { email: { contains: search, mode: 'insensitive' } }
    ];
  }
  
  // Use select to limit returned fields
  const customers = await database.customer.findMany({
    where,
    select: {
      id: true,
      email: true,
      firstName: true,
      lastName: true,
      createdAt: true
    },
    orderBy: { createdAt: 'desc' },
    take: 50
  });
  
  return successResponse(customers);
}

API Documentation

OpenAPI Specification

// lib/openapi.ts
export const openAPISpec = {
  openapi: '3.0.0',
  info: {
    title: 'DeelRx CRM API',
    version: '1.0.0',
    description: 'API for DeelRx CRM system'
  },
  servers: [
    {
      url: 'https://api.deelrxcrm.app',
      description: 'Production server'
    },
    {
      url: 'http://localhost:3000',
      description: 'Development server'
    }
  ],
  paths: {
    '/api/customers': {
      get: {
        summary: 'List customers',
        parameters: [
          {
            name: 'page',
            in: 'query',
            schema: { type: 'integer', default: 1 }
          },
          {
            name: 'limit',
            in: 'query',
            schema: { type: 'integer', default: 10 }
          }
        ],
        responses: {
          '200': {
            description: 'List of customers',
            content: {
              'application/json': {
                schema: {
                  type: 'object',
                  properties: {
                    data: {
                      type: 'array',
                      items: { $ref: '#/components/schemas/Customer' }
                    },
                    pagination: { $ref: '#/components/schemas/Pagination' }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  components: {
    schemas: {
      Customer: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          email: { type: 'string', format: 'email' },
          firstName: { type: 'string' },
          lastName: { type: 'string' },
          createdAt: { type: 'string', format: 'date-time' }
        }
      },
      Pagination: {
        type: 'object',
        properties: {
          page: { type: 'integer' },
          limit: { type: 'integer' },
          total: { type: 'integer' },
          pages: { type: 'integer' }
        }
      }
    }
  }
};

API Testing

// tests/api/customers.test.ts
import { describe, it, expect } from 'vitest';

describe('Customers API', () => {
  it('should list customers', async () => {
    const response = await fetch('/api/customers', {
      headers: {
        'Authorization': 'Bearer test-token'
      }
    });
    
    expect(response.status).toBe(200);
    
    const data = await response.json();
    expect(data.data).toBeDefined();
    expect(Array.isArray(data.data)).toBe(true);
  });
  
  it('should create a customer', async () => {
    const customerData = {
      email: '[email protected]',
      firstName: 'John',
      lastName: 'Doe'
    };
    
    const response = await fetch('/api/customers', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer test-token'
      },
      body: JSON.stringify(customerData)
    });
    
    expect(response.status).toBe(201);
    
    const data = await response.json();
    expect(data.data.email).toBe(customerData.email);
  });
});

Best Practices

API Design

  • Use consistent URL patterns and HTTP methods
  • Implement proper error handling and status codes
  • Validate all input data with schemas
  • Use appropriate HTTP status codes
  • Implement rate limiting and security measures

Performance

  • Implement caching for frequently accessed data
  • Use database query optimization
  • Limit response payload sizes
  • Use pagination for large datasets
  • Monitor API performance and errors

Security

  • Authenticate all API requests
  • Validate and sanitize all input data
  • Implement proper authorization checks
  • Use HTTPS for all communications
  • Log security events and monitor for attacks

Troubleshooting

Common Issues

Check Clerk configuration, verify API keys, and ensure proper token validation. Review user permissions and organization membership.
Verify database URL, check connection pool settings, and monitor database performance. Ensure proper indexing for queries.
Analyze slow queries, implement caching strategies, and optimize database operations. Monitor API response times and error rates.

Debugging Tools

# API testing with curl
curl -X GET "http://localhost:3000/api/customers" \
  -H "Authorization: Bearer your-token"

# Database query analysis
pnpm db:studio

# API documentation
pnpm docs:dev

Next Steps