Files
schulos/docs/API_STANDARDIZATION.md

6.9 KiB

API Standardization Guide

Overview

All API routes must follow this standardized format for consistency, maintainability, and predictable client-side behavior.

Response Format

Success Response (GET, POST, PUT, PATCH)

{
  "success": true,
  "data": { /* actual data */ },
  "timestamp": "2024-01-01T12:00:00Z",
  "path": "/api/v1/students"
}

Error Response

{
  "success": false,
  "error": "Descriptive error message",
  "timestamp": "2024-01-01T12:00:00Z",
  "path": "/api/v1/students"
}

Delete Response (204 No Content)

No body, just status 204

Status Codes

Code Meaning Use Case
200 OK GET, PUT, PATCH success
201 Created POST success
204 No Content DELETE success
400 Bad Request Invalid input data
401 Unauthorized Not authenticated
403 Forbidden Authenticated but no permission
404 Not Found Resource doesn't exist
422 Unprocessable Validation error
500 Server Error Unhandled exception

Route Structure

Before (Inconsistent)

export async function POST(request: Request) {
  try {
    const session = await getSession();
    if (!session) {
      return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
    }

    // ... 100 lines of mixed logic

    if (!resource) {
      return NextResponse.json({ error: 'Not found' }, { status: 404 });
    }

    // Multiple different response formats
    return NextResponse.json(resource, { status: 201 });
  } catch (error) {
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
  }
}

After (Standardized)

import { NextRequest } from 'next/server';
import { withMiddleware, withAuth, withRoles, withAudit } from '@/lib/route-middleware';
import { successResponse, createdResponse, notFoundError, validationError } from '@/lib/api-response';
import { db } from '@/lib/db';

async function handler(request: NextRequest) {
  const body = await request.json();
  const { resourceId } = body;

  if (!resourceId) {
    return validationError('resourceId is required');
  }

  const resource = await db.resource.create({ data: body });

  if (!resource) {
    return notFoundError('Resource creation failed');
  }

  return createdResponse(resource);
}

export const POST = withMiddleware(
  withAuth,
  withRoles(['TEACHER', 'ADMIN']),
  withAudit('CREATE_RESOURCE')
)(handler);

Using API Response Helpers

All routes should use helpers from @/lib/api-response:

import {
  successResponse,
  createdResponse,
  noContentResponse,
  unauthorizedError,
  forbiddenError,
  notFoundError,
  validationError,
  serverError,
} from '@/lib/api-response';

// GET - return data
return successResponse(data);

// POST - return created resource
return createdResponse(resource);

// DELETE - no content
return noContentResponse();

// Errors
return unauthorizedError();
return forbiddenError();
return notFoundError('Resource not found');
return validationError('Invalid email format');
return serverError(error);

Using Middleware

Basic Authentication

import { withAuth } from '@/lib/route-middleware';

async function handler(request: NextRequest) {
  const user = (request as any).user; // injected by middleware
  // handler logic
}

export const GET = withAuth(handler);

Role-Based Access

import { withRoles } from '@/lib/route-middleware';

async function handler(request: NextRequest) {
  // only TEACHER and ADMIN can access
}

export const POST = withRoles(['TEACHER', 'ADMIN'])(handler);

Audit Logging

import { withAudit } from '@/lib/route-middleware';

export const DELETE = withAudit('DELETE_STUDENT')(handler);

Combining Middleware

import { withMiddleware, withAuth, withRoles, withAudit } from '@/lib/route-middleware';

export const PUT = withMiddleware(
  withAuth,
  withRoles(['TEACHER', 'ADMIN']),
  withAudit('UPDATE_GRADES')
)(handler);

Client-Side Usage

With useApiGet (for data fetching)

import { useApiGet } from '@/lib/hooks/useApi';

function MyComponent() {
  const { data, error, isLoading, mutate } = useApiGet('/api/v1/students');

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>{data.map(s => <div key={s.id}>{s.name}</div>)}</div>;
}

With useApiMutation (for mutations)

import { useApiMutation } from '@/lib/hooks/useApi';

function MyComponent() {
  const { mutate, isLoading, error } = useApiMutation('/api/v1/students', {
    method: 'POST',
    onSuccess: (data) => console.log('Created:', data),
    onError: (error) => console.error('Error:', error),
  });

  const handleCreate = async () => {
    await mutate({ name: 'John', email: 'john@example.com' });
  };

  return <button onClick={handleCreate} disabled={isLoading}>Create</button>;
}

Migration Checklist

When standardizing an existing route:

  • Import helpers from @/lib/api-response
  • Import middleware from @/lib/route-middleware
  • Replace inline auth check with withAuth
  • Replace inline role check with withRoles
  • Replace inline error handling with response helpers
  • Add withAudit for sensitive operations
  • Ensure all responses use standardized format
  • Test with client using new useApiGet/useApiMutation
  • Update any direct fetch calls to use hooks

Common Patterns

List with Filtering/Pagination

async function handler(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get('page') || '1');
  const limit = parseInt(searchParams.get('limit') || '20');

  const [data, total] = await Promise.all([
    db.resource.findMany({
      skip: (page - 1) * limit,
      take: limit,
    }),
    db.resource.count(),
  ]);

  return successResponse({
    data,
    pagination: { page, limit, total, pages: Math.ceil(total / limit) },
  });
}

Nested Route with ID

async function handler(request: NextRequest, { params }: any) {
  const { id } = await params;

  const resource = await db.resource.findUnique({ where: { id } });

  if (!resource) {
    return notFoundError(`Resource ${id} not found`);
  }

  return successResponse(resource);
}

Batch Operations

async function handler(request: NextRequest) {
  const { ids } = await request.json();

  const deleted = await db.resource.deleteMany({
    where: { id: { in: ids } },
  });

  return successResponse({
    deletedCount: deleted.count,
    ids,
  });
}

Benefits

  1. Predictable - Clients know exactly what format to expect
  2. Debuggable - Timestamps and paths help track issues
  3. DRY - Reusable middleware eliminates boilerplate
  4. Testable - Standardized responses are easier to test
  5. Maintainable - Consistent patterns across all routes
  6. Safe - Automatic error handling reduces bugs