Building Scalable APIs with Next.js 14

Learn how to build production-ready APIs using Next.js 14's App Router and Route Handlers with best practices for authentication, validation, and error handling.

JaziraTech Team
January 15, 2025
5 min read

Building Scalable APIs with Next.js 14

Next.js 14 revolutionizes API development with its powerful Route Handlers and App Router. In this comprehensive guide, we'll explore how to build production-ready APIs that scale.

Why Next.js for APIs?

Next.js offers several advantages for API development:

  • Type Safety: Full TypeScript support out of the box
  • Edge Runtime: Deploy APIs closer to your users
  • Built-in Middleware: Authentication and validation made easy
  • Automatic API Routes: File-based routing for APIs
  • Integrated Deployment: Seamless deployment with Vercel

Setting Up Your API Structure

Let's start with a well-organized API structure:

import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { prisma } from '@/lib/prisma';
import { authenticate } from '@/lib/auth';

// Input validation schema
const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(100),
  role: z.enum(['user', 'admin']).optional(),
});

export async function POST(request: NextRequest) {
  try {
    // Authentication
    const user = await authenticate(request);
    if (!user) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      );
    }

    // Parse and validate input
    const body = await request.json();
    const validatedData = createUserSchema.parse(body);

    // Create user in database
    const newUser = await prisma.user.create({
      data: validatedData,
    });

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

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

Implementing Authentication

Here's a robust authentication middleware:

import { NextRequest } from 'next/server';
import jwt from 'jsonwebtoken';

export async function authenticate(request: NextRequest) {
  const token = request.headers.get('authorization')?.replace('Bearer ', '');
  
  if (!token) {
    return null;
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!);
    return decoded as { id: string; email: string };
  } catch {
    return null;
  }
}

Error Handling Best Practices

Create a centralized error handler:

export class APIError extends Error {
  constructor(
    public statusCode: number,
    public message: string,
    public details?: any
  ) {
    super(message);
    this.name = 'APIError';
  }
}

export function handleAPIError(error: unknown): NextResponse {
  if (error instanceof APIError) {
    return NextResponse.json(
      { error: error.message, details: error.details },
      { status: error.statusCode }
    );
  }

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

Rate Limiting

Protect your API with rate limiting:

import { NextRequest, NextResponse } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'),
});

export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api')) {
    const ip = request.ip ?? '127.0.0.1';
    const { success } = await ratelimit.limit(ip);
    
    if (!success) {
      return NextResponse.json(
        { error: 'Too many requests' },
        { status: 429 }
      );
    }
  }
  
  return NextResponse.next();
}

Database Connection Pooling

Optimize database connections:

import { PrismaClient } from '@prisma/client';

const globalForPrisma = global as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
  });

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma;
}

API Versioning Strategy

Implement versioning for backward compatibility:

export async function GET(
  request: NextRequest,
  { params }: { params: { version: string } }
) {
  const version = params.version;
  
  switch (version) {
    case 'v1':
      return handleV1Request(request);
    case 'v2':
      return handleV2Request(request);
    default:
      return NextResponse.json(
        { error: 'Invalid API version' },
        { status: 400 }
      );
  }
}

Testing Your APIs

Write comprehensive tests:

import { createMocks } from 'node-mocks-http';
import { POST } from '@/app/api/v1/users/route';

describe('/api/v1/users', () => {
  it('should create a new user', async () => {
    const { req } = createMocks({
      method: 'POST',
      headers: {
        'authorization': 'Bearer valid-token',
      },
      body: {
        email: 'test@example.com',
        name: 'Test User',
      },
    });

    const response = await POST(req as any);
    const data = await response.json();

    expect(response.status).toBe(201);
    expect(data.email).toBe('test@example.com');
  });
});

Deployment Considerations

Environment Variables

DATABASE_URL="postgresql://..."
JWT_SECRET="your-secret-key"
REDIS_URL="redis://..."
API_RATE_LIMIT="100"

Monitoring and Logging

import winston from 'winston';

export const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'api.log' }),
  ],
});

Performance Optimization Tips

  1. Use Edge Runtime for lightweight APIs
  2. Implement caching with Redis or in-memory stores
  3. Optimize database queries with proper indexing
  4. Use streaming for large responses
  5. Enable compression for API responses

Conclusion

Building scalable APIs with Next.js 14 provides a robust foundation for modern applications. By following these best practices, you'll create APIs that are secure, performant, and maintainable.

Next Steps

  • Explore GraphQL integration with Next.js
  • Learn about WebSocket support
  • Implement API documentation with OpenAPI

Happy coding! ๐Ÿš€

Share this article

About the author

JaziraTech Team

Content Team

The technical content team at JaziraTech, sharing insights on API development, AI integration, and modern web technologies.