Skip to main content
Back to articles

Development

How to Build Production-Ready REST APIs with Next.js and TypeScript

A comprehensive guide to building scalable, type-safe REST APIs using Next.js App Router and TypeScript. Learn routing, validation, error handling, and best practices.

November 14, 202412 min
Modern API architecture with TypeScript code, dark theme, abstract tech visualization

Next.js 14+ with App Router provides powerful tools for building production-ready REST APIs. Combined with TypeScript's type safety, you can create scalable, maintainable backend services with excellent developer experience.

Why Next.js for REST APIs?

While Next.js is primarily known as a React framework, its App Router introduces route handlers that make it an excellent choice for building APIs:

  • Colocation: Keep your API routes alongside frontend code
  • Type Safety: Share TypeScript types between frontend and backend
  • Edge Runtime: Deploy API routes to the edge for low latency
  • Built-in Middleware: Request/response handling without external frameworks

Setting Up Route Handlers

Route handlers in Next.js are defined using route.ts files in the app/api directory:

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const users = await db.user.findMany();
  return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const user = await db.user.create({ data: body });
  return NextResponse.json(user, { status: 201 });
}

Request Validation with Zod

Never trust user input. Use Zod for runtime validation and TypeScript type inference:

import { z } from 'zod';

const userSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
  age: z.number().int().positive().optional(),
});

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const validated = userSchema.parse(body);

    // validated is now type-safe
    const user = await createUser(validated);
    return NextResponse.json(user, { status: 201 });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Validation failed', details: error.issues },
        { status: 400 }
      );
    }
    throw error;
  }
}

Error Handling Best Practices

Consistent error handling improves API reliability and developer experience:

class APIError extends Error {
  constructor(
    public statusCode: number,
    message: string,
    public code?: string
  ) {
    super(message);
  }
}

export async function GET(request: NextRequest) {
  try {
    const data = await fetchData();
    return NextResponse.json(data);
  } catch (error) {
    if (error instanceof APIError) {
      return NextResponse.json(
        { error: error.message, code: error.code },
        { status: error.statusCode }
      );
    }

    // Log unexpected errors
    console.error('Unexpected API error:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Rate Limiting

Protect your API from abuse with rate limiting. Here's a simple in-memory implementation:

const rateLimit = new Map<string, { count: number; resetAt: number }>();

function checkRateLimit(ip: string, limit = 100, window = 60000): boolean {
  const now = Date.now();
  const record = rateLimit.get(ip);

  if (!record || now > record.resetAt) {
    rateLimit.set(ip, { count: 1, resetAt: now + window });
    return true;
  }

  if (record.count >= limit) {
    return false;
  }

  record.count++;
  return true;
}

export async function POST(request: NextRequest) {
  const ip = request.headers.get('x-forwarded-for') || 'unknown';

  if (!checkRateLimit(ip)) {
    return NextResponse.json(
      { error: 'Too many requests' },
      { status: 429 }
    );
  }

  // Process request...
}

Authentication with JWT

Implement JWT-based authentication for secure API access:

import jwt from 'jsonwebtoken';

export async function POST(request: NextRequest) {
  const token = request.headers.get('Authorization')?.split(' ')[1];

  if (!token) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET!);
    const userId = (payload as any).sub;

    // Use userId for authorized request
    const data = await fetchUserData(userId);
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: 'Invalid token' },
      { status: 401 }
    );
  }
}

Database Integration

Use Prisma for type-safe database access that integrates perfectly with TypeScript:

// app/api/posts/route.ts
import { prisma } from '@/lib/prisma';
import { z } from 'zod';

const postSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(1),
  published: z.boolean().default(false),
});

export async function POST(request: NextRequest) {
  const body = await request.json();
  const validated = postSchema.parse(body);

  const post = await prisma.post.create({
    data: validated,
    include: { author: true },
  });

  return NextResponse.json(post, { status: 201 });
}

Testing Your API

Write integration tests to ensure API reliability:

import { POST } from './route';
import { NextRequest } from 'next/server';

describe('POST /api/users', () => {
  it('creates user with valid data', async () => {
    const request = new NextRequest('http://localhost/api/users', {
      method: 'POST',
      body: JSON.stringify({
        email: 'test@example.com',
        name: 'Test User',
      }),
    });

    const response = await POST(request);
    const data = await response.json();

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

  it('rejects invalid email', async () => {
    const request = new NextRequest('http://localhost/api/users', {
      method: 'POST',
      body: JSON.stringify({
        email: 'invalid-email',
        name: 'Test User',
      }),
    });

    const response = await POST(request);
    expect(response.status).toBe(400);
  });
});

Deployment Considerations

When deploying your Next.js API to production:

  • Environment Variables: Use .env.production for secrets
  • CORS: Configure allowed origins for cross-origin requests
  • Logging: Implement structured logging (e.g., Pino, Winston)
  • Monitoring: Use Sentry or similar for error tracking
  • Caching: Implement Redis for session storage and caching

Performance Optimization

Optimize API performance with these strategies:

  • Use Edge Runtime for globally distributed APIs
  • Implement response caching with next.revalidate
  • Use database connection pooling
  • Minimize middleware overhead
  • Consider API pagination for large datasets

Conclusion

Next.js provides a robust foundation for building production-ready REST APIs. By combining TypeScript's type safety, Zod validation, proper error handling, and modern security practices, you can create APIs that are both developer-friendly and production-ready.

At Aivoma, we run Build, Integration & Migration Kits that cover API platform engineering, observability, and rollout planning. If you need a senior team to harden your Next.js APIs or migrate legacy endpoints, we can plug in quickly.

Book a working session and we’ll send back a KPI-driven plan within 48 hours.

Cost planning

Software Project Estimate Calculator

Quickly map MVP scope, delivery window, and engineering cost with sliders tuned for AI-native builds.

Open the calculator

Brevo double opt-in delivers the PDF recap + assumptions.

Related links

Need help shipping this?

Aivoma delivers custom software, performance tuning, and DevOps automation in 6–12 weeks. Let's map the milestones for your team.

Book a consultation