Skip to main content

API Routes Wrapper

Overview

The API Routes wrapper (withFortress) provides route-level security validation for Next.js API Routes with custom configuration per endpoint.

Why Use the Wrapper

While middleware protects all routes globally, the wrapper allows:

  • Custom rate limits per endpoint
  • CSRF validation for specific routes
  • Custom payload limits per endpoint
  • Method restrictions per route
  • Encoding validation toggle per endpoint

Implementation

/**
* Create a secure API route wrapper
*/
export function createWithFortress(config: FortressConfig) {
const logger = new FortressLogger(config.logging);
const deserializationValidator = createDeserializationValidator(
config.modules.deserialization
);
const injectionValidator = createInjectionValidator(config.modules.injection);
const csrfValidator = config.modules.csrf.enabled
? createCSRFValidator(config.modules.csrf)
: null;
const encodingValidator = createEncodingValidator(config.modules.encoding);

return function withFortress(
handler: (request: NextRequest) => Promise<Response>,
options: SecureRouteOptions = {}
) {
return async function securedHandler(
request: NextRequest
): Promise<Response> {
// 1. Check allowed methods
if (options.allowedMethods) {
if (!options.allowedMethods.includes(request.method)) {
return new NextResponse('Method Not Allowed', { status: 405 });
}
}

// 2. Rate limiting (if specified)
if (options.rateLimit) {
const rateLimitResult = checkRateLimit(request, options.rateLimit);
if (!rateLimitResult.allowed) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
'Retry-After': String(
Math.ceil(rateLimitResult.retryAfter / 1000)
),
},
});
}
}

// 3. Payload size check
const contentLength = request.headers.get('content-length');
const maxSize =
options.maxPayloadSize || config.modules.content.maxPayloadSize;

if (contentLength && parseInt(contentLength) > maxSize) {
return new NextResponse('Payload Too Large', { status: 413 });
}

// 4. Encoding validation
if (
options.validateEncoding !== false &&
config.modules.encoding.enabled
) {
const contentType = request.headers.get('content-type');
const body = await request.clone().arrayBuffer();

const encodingResult = await encodingValidator.validate(
contentType,
body
);
if (!encodingResult.valid) {
return new NextResponse('Bad Request: Invalid Encoding', {
status: 400,
});
}
}

// 5. Parse and validate request body
if (['POST', 'PUT', 'PATCH'].includes(request.method)) {
const validationResult = await validateRequestBody(
request,
deserializationValidator,
injectionValidator,
config
);

if (!validationResult.valid) {
return new NextResponse('Forbidden', { status: 403 });
}
}

// 6. CSRF validation (if required)
if (options.requireCSRF && csrfValidator) {
const csrfToken =
request.headers.get(csrfValidator.getHeaderName()) ||
request.cookies.get(csrfValidator.getCookieName())?.value;

const sessionId = request.cookies.get('session')?.value || 'default';

const csrfResult = await csrfValidator.validate(
csrfToken,
sessionId,
request.method
);
if (!csrfResult.valid) {
return new NextResponse('Forbidden: Invalid CSRF Token', {
status: 403,
});
}
}

// Execute the original handler
return await handler(request);
};
};
}

Basic Usage

Setup

// fortress.config.ts
import { FortressConfig } from '@mindfiredigital/nextjs-fortress';

export const fortressConfig: FortressConfig = {
enabled: true,
mode: 'development',
// ... your config
};

// lib/fortress.ts
import { createWithFortress } from '@mindfiredigital/nextjs-fortress';
import { fortressConfig } from '../fortress.config';

export const withFortress = createWithFortress(fortressConfig);

Simple Protection

// app/api/user/route.ts
import { withFortress } from '@/lib/fortress';

export const POST = withFortress(async (request) => {
const data = await request.json();

// Data already validated:
// ✓ No prototype pollution
// ✓ No SQL injection
// ✓ No XSS
// ✓ Valid encoding

await updateUser(data);
return Response.json({ success: true });
});

Advanced Configuration

Custom Rate Limit

// app/api/auth/login/route.ts
import { withFortress } from '@/lib/fortress';

export const POST = withFortress(
async (request) => {
const { username, password } = await request.json();
const user = await authenticateUser(username, password);
return Response.json({ success: true, user });
},
{
rateLimit: {
requests: 5, // Only 5 attempts
window: 300000, // Per 5 minutes
},
}
);

Method Restrictions

// app/api/data/route.ts
import { withFortress } from '@/lib/fortress';

export const handler = withFortress(
async (request) => {
if (request.method === 'GET') {
return Response.json(await getData());
}

if (request.method === 'POST') {
const data = await request.json();
return Response.json(await createData(data));
}
},
{
allowedMethods: ['GET', 'POST'], // Only these methods
}
);

export { handler as GET, handler as POST };

Custom Payload Size

// app/api/upload/route.ts
import { withFortress } from '@/lib/fortress';

export const POST = withFortress(
async (request) => {
const formData = await request.formData();
const file = formData.get('file');
return Response.json({ success: true });
},
{
maxPayloadSize: 10 * 1024 * 1024, // 10MB for file uploads
}
);

CSRF Protection

// app/api/account/delete/route.ts
import { withFortress } from '@/lib/fortress';

export const POST = withFortress(
async (request) => {
const { userId } = await request.json();
await deleteAccount(userId);
return Response.json({ success: true });
},
{
requireCSRF: true, // Require valid CSRF token
}
);

Disable Encoding Validation

// app/api/webhook/route.ts
import { withFortress } from '@/lib/fortress';

export const POST = withFortress(
async (request) => {
// Handle webhook from third-party
const data = await request.text();
return Response.json({ received: true });
},
{
validateEncoding: false, // Skip encoding check for webhooks
}
);

Combined Options

// app/api/admin/users/route.ts
import { withFortress } from '@/lib/fortress';

export const POST = withFortress(
async (request) => {
const data = await request.json();
await createUser(data);
return Response.json({ success: true });
},
{
requireCSRF: true,
allowedMethods: ['POST'],
rateLimit: {
requests: 10,
window: 60000,
},
maxPayloadSize: 50 * 1024, // 50KB
}
);

Options Reference

interface SecureRouteOptions {
// Require CSRF token validation
requireCSRF?: boolean;

// Custom rate limit for this endpoint
rateLimit?: {
requests: number;
window: number; // milliseconds
};

// Maximum payload size
maxPayloadSize?: number;

// Allowed HTTP methods
allowedMethods?: string[];

// Enable/disable encoding validation
validateEncoding?: boolean;
}

Real-World Examples

E-Commerce Checkout

// app/api/checkout/route.ts
import { withFortress } from '@/lib/fortress';

export const POST = withFortress(
async (request) => {
const order = await request.json();

// Validated data - safe to process
const result = await processPayment(order);

return Response.json(result);
},
{
requireCSRF: true, // Prevent CSRF
maxPayloadSize: 100 * 1024, // 100KB max
rateLimit: {
requests: 3, // 3 checkouts
window: 60000, // per minute
},
}
);

User Registration

// app/api/auth/register/route.ts
import { withFortress } from '@/lib/fortress';

export const POST = withFortress(
async (request) => {
const userData = await request.json();

// Already validated:
// - No prototype pollution
// - No SQL injection in fields
// - No XSS in inputs

const user = await createUser(userData);
return Response.json({ success: true, user });
},
{
rateLimit: {
requests: 5, // 5 registrations
window: 3600000, // per hour
},
maxPayloadSize: 10 * 1024, // Small registration data
}
);

Admin API

// app/api/admin/settings/route.ts
import { withFortress } from '@/lib/fortress';

export const POST = withFortress(
async (request) => {
const settings = await request.json();
await updateSettings(settings);
return Response.json({ success: true });
},
{
requireCSRF: true, // Critical - require CSRF
allowedMethods: ['POST'], // Only POST
rateLimit: {
requests: 20,
window: 60000,
},
}
);

Error Responses

Method Not Allowed

// Request: DELETE /api/user
// Response:
Status: 405 Method Not Allowed

Rate Limit Exceeded

// Response:
Status: 429 Too Many Requests
Retry-After: 45

// Headers indicate when to retry

Payload Too Large

// Response:
Status: 413 Payload Too Large

Invalid Encoding

// Response:
Status: 400 Bad Request
Body: "Bad Request: Invalid Encoding"

Validation Failed

// Response:
Status: 403 Forbidden

CSRF Invalid

// Response:
Status: 403 Forbidden
Body: "Forbidden: Invalid CSRF Token"

Summary

withFortress wrapper provides:

  • Route-level protection
  • Custom rate limits
  • CSRF validation
  • Method restrictions
  • Payload size limits
  • Encoding validation

How to use:

export const POST = withFortress(
async (request) => {
// Your handler
},
{
requireCSRF: true,
rateLimit: { requests: 10, window: 60000 },
}
);

Related Documentation: