Menu

React & Next.js Topic

Next.js API Routes

Next.js API Routes allow you to build your backend API directly within your Next.js application. Each file in the pages/api directory becomes an API endpoint that serverless functions can handle.

Basic Structure

text

pages/
  api/
    users/
      index.js          # /api/users
      [id].js          # /api/users/123
    auth/
      login.js         # /api/auth/login
    middleware.js      # Custom middleware

1. Creating Backend Endpoints

Basic API Route

jsx

// pages/api/hello.js
export default function handler(req, res) {
  res.status(200).json({ message: 'Hello from API!' });
}

// Access at: /api/hello

HTTP Methods Handling

jsx

// pages/api/users.js
export default function handler(req, res) {
  const { method } = req;

  switch (method) {
    case 'GET':
      handleGet(req, res);
      break;
    case 'POST':
      handlePost(req, res);
      break;
    case 'PUT':
      handlePut(req, res);
      break;
    case 'DELETE':
      handleDelete(req, res);
      break;
    default:
      res.setHeader('Allow', ['GET', 'POST', 'PUT', 'DELETE']);
      res.status(405).end(`Method ${method} Not Allowed`);
  }
}

async function handleGet(req, res) {
  try {
    // Fetch users from database
    const users = await fetchUsersFromDB();
    res.status(200).json(users);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch users' });
  }
}

async function handlePost(req, res) {
  try {
    const { name, email } = req.body;
    
    // Validate input
    if (!name || !email) {
      return res.status(400).json({ error: 'Name and email are required' });
    }

    // Create user in database
    const newUser = await createUserInDB({ name, email });
    res.status(201).json(newUser);
  } catch (error) {
    res.status(500).json({ error: 'Failed to create user' });
  }
}

// Mock database functions
async function fetchUsersFromDB() {
  return [{ id: 1, name: 'John', email: 'john@example.com' }];
}

async function createUserInDB(userData) {
  return { id: Date.now(), ...userData, createdAt: new Date() };
}

Dynamic Routes

jsx

// pages/api/users/[id].js
export default function handler(req, res) {
  const {
    query: { id },
    method
  } = req;

  switch (method) {
    case 'GET':
      getUserById(req, res, id);
      break;
    case 'PUT':
      updateUser(req, res, id);
      break;
    case 'DELETE':
      deleteUser(req, res, id);
      break;
    default:
      res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
      res.status(405).end(`Method ${method} Not Allowed`);
  }
}

async function getUserById(req, res, id) {
  try {
    const user = await findUserById(parseInt(id));
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    res.status(200).json(user);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch user' });
  }
}

async function updateUser(req, res, id) {
  try {
    const updates = req.body;
    const updatedUser = await updateUserInDB(parseInt(id), updates);
    
    if (!updatedUser) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    res.status(200).json(updatedUser);
  } catch (error) {
    res.status(500).json({ error: 'Failed to update user' });
  }
}

// Mock database functions
async function findUserById(id) {
  const users = [{ id: 1, name: 'John', email: 'john@example.com' }];
  return users.find(user => user.id === id);
}

async function updateUserInDB(id, updates) {
  const user = await findUserById(id);
  return user ? { ...user, ...updates, updatedAt: new Date() } : null;
}

API Routes with App Router (Next.js 13+)

jsx

// app/api/users/route.js
import { NextResponse } from 'next/server';

export async function GET() {
  const users = await fetchUsersFromDB();
  return NextResponse.json(users);
}

export async function POST(request) {
  try {
    const body = await request.json();
    const { name, email } = body;

    if (!name || !email) {
      return NextResponse.json(
        { error: 'Name and email are required' },
        { status: 400 }
      );
    }

    const newUser = await createUserInDB({ name, email });
    return NextResponse.json(newUser, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to create user' },
      { status: 500 }
    );
  }
}

// app/api/users/[id]/route.js
export async function GET(request, { params }) {
  const { id } = params;
  const user = await findUserById(parseInt(id));
  
  if (!user) {
    return NextResponse.json(
      { error: 'User not found' },
      { status: 404 }
    );
  }
  
  return NextResponse.json(user);
}

export async function PUT(request, { params }) {
  const { id } = params;
  const updates = await request.json();
  
  const updatedUser = await updateUserInDB(parseInt(id), updates);
  
  if (!updatedUser) {
    return NextResponse.json(
      { error: 'User not found' },
      { status: 404 }
    );
  }
  
  return NextResponse.json(updatedUser);
}

2. Middleware for Authentication & Logging

Next.js Middleware allows you to run code before a request is completed. It's perfect for authentication, logging, and request modification.

Basic Middleware Setup

jsx

// middleware.js (root of your project)
import { NextResponse } from 'next/server';

export function middleware(request) {
  // Clone the request headers
  const requestHeaders = new Headers(request.headers);
  
  // Add new request headers
  requestHeaders.set('x-current-path', request.nextUrl.pathname);

  // You can also set response headers
  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });

  response.headers.set('x-custom-header', 'my-custom-value');
  
  return response;
}

// Specify which paths middleware should run on
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api/auth (authentication routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api/auth|_next/static|_next/image|favicon.ico).*)',
  ],
};

Authentication Middleware

jsx

// middleware.js
import { NextResponse } from 'next/server';
import { verifyToken } from './lib/auth';

// List of paths that don't require authentication
const publicPaths = [
  '/',
  '/login',
  '/register',
  '/api/auth/login',
  '/api/auth/register'
];

export async function middleware(request) {
  const { pathname } = request.nextUrl;

  // Check if the path is public
  const isPublicPath = publicPaths.some(path => 
    pathname === path || pathname.startsWith('/api/auth')
  );

  if (isPublicPath) {
    return NextResponse.next();
  }

  // Get token from cookie or header
  const token = request.cookies.get('auth-token')?.value || 
                request.headers.get('authorization')?.replace('Bearer ', '');

  if (!token) {
    // Redirect to login if no token
    return NextResponse.redirect(new URL('/login', request.url));
  }

  try {
    // Verify the token
    const decoded = await verifyToken(token);
    
    // Add user info to request headers
    const requestHeaders = new Headers(request.headers);
    requestHeaders.set('x-user-id', decoded.userId);
    requestHeaders.set('x-user-email', decoded.email);
    requestHeaders.set('x-user-role', decoded.role);

    const response = NextResponse.next({
      request: {
        headers: requestHeaders,
      },
    });

    return response;
  } catch (error) {
    // Token is invalid
    const response = NextResponse.redirect(new URL('/login', request.url));
    
    // Clear invalid token
    response.cookies.delete('auth-token');
    
    return response;
  }
}

export const config = {
  matcher: [
    '/dashboard/:path*',
    '/profile/:path*',
    '/api/users/:path*',
    '/api/posts/:path*'
  ],
};

Role-Based Access Control Middleware

jsx

// middleware/role-auth.js
import { NextResponse } from 'next/server';

// Define routes and required roles
const routePermissions = {
  '/api/admin': ['admin'],
  '/api/users': ['admin', 'moderator'],
  '/api/posts': ['admin', 'moderator', 'user'],
  '/dashboard/admin': ['admin'],
  '/dashboard': ['admin', 'moderator', 'user'],
};

export function roleAuthMiddleware(request) {
  const { pathname } = request.nextUrl;
  const userRole = request.headers.get('x-user-role');

  // Find the most specific matching route
  let requiredRoles = ['user']; // Default role
  
  for (const [route, roles] of Object.entries(routePermissions)) {
    if (pathname.startsWith(route)) {
      requiredRoles = roles;
      break;
    }
  }

  // Check if user has required role
  if (!requiredRoles.includes(userRole)) {
    return NextResponse.json(
      { error: 'Insufficient permissions' },
      { status: 403 }
    );
  }

  return NextResponse.next();
}

Logging Middleware

jsx

// middleware/logging.js
import { NextResponse } from 'next/server';

export async function loggingMiddleware(request) {
  const start = Date.now();
  const { method, url } = request;
  
  // Process the request
  const response = NextResponse.next();
  
  const duration = Date.now() - start;
  
  // Log request details
  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    method,
    url: url,
    path: request.nextUrl.pathname,
    status: response.status,
    duration: `${duration}ms`,
    userAgent: request.headers.get('user-agent'),
    ip: request.ip || request.headers.get('x-forwarded-for'),
    userId: request.headers.get('x-user-id') || 'anonymous'
  }));

  return response;
}

Comprehensive Middleware with Multiple Functions

jsx

// middleware.js
import { NextResponse } from 'next/server';
import { verifyToken } from './lib/auth';

// Chain multiple middleware functions
export async function middleware(request) {
  // 1. Logging
  const response = await loggingMiddleware(request);
  
  // 2. Authentication (skip for public paths)
  if (!isPublicPath(request.nextUrl.pathname)) {
    const authResponse = await authMiddleware(request);
    if (authResponse.status !== 200) {
      return authResponse;
    }
  }
  
  // 3. Role-based access control
  const roleResponse = roleAuthMiddleware(request);
  if (roleResponse.status !== 200) {
    return roleResponse;
  }

  // 4. CORS headers for API routes
  if (request.nextUrl.pathname.startsWith('/api/')) {
    return corsMiddleware(request, response);
  }

  return response;
}

// Individual middleware functions
async function loggingMiddleware(request) {
  const start = Date.now();
  const response = NextResponse.next();
  const duration = Date.now() - start;

  console.log(`${request.method} ${request.nextUrl.pathname} - ${duration}ms`);
  
  return response;
}

async function authMiddleware(request) {
  const token = request.cookies.get('auth-token')?.value;

  if (!token) {
    if (request.nextUrl.pathname.startsWith('/api/')) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }
    return NextResponse.redirect(new URL('/login', request.url));
  }

  try {
    const decoded = await verifyToken(token);
    const requestHeaders = new Headers(request.headers);
    requestHeaders.set('x-user-id', decoded.userId);
    requestHeaders.set('x-user-email', decoded.email);
    requestHeaders.set('x-user-role', decoded.role);

    return NextResponse.next({
      request: { headers: requestHeaders },
    });
  } catch (error) {
    const response = request.nextUrl.pathname.startsWith('/api/') 
      ? NextResponse.json({ error: 'Invalid token' }, { status: 401 })
      : NextResponse.redirect(new URL('/login', request.url));
    
    response.cookies.delete('auth-token');
    return response;
  }
}

function corsMiddleware(request, response) {
  // Add CORS headers
  response.headers.set('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGINS || '*');
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  response.headers.set('Access-Control-Allow-Credentials', 'true');

  // Handle preflight requests
  if (request.method === 'OPTIONS') {
    return new Response(null, { status: 200, headers: response.headers });
  }

  return response;
}

function isPublicPath(pathname) {
  const publicPaths = ['/', '/login', '/register', '/api/auth', '/_next', '/favicon.ico'];
  return publicPaths.some(path => pathname.startsWith(path));
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     */
    '/((?!_next/static|_next/image).*)',
  ],
};

API Route with Authentication

jsx

// pages/api/protected/user-profile.js
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../auth/[...nextauth]';

export default async function handler(req, res) {
  // Check authentication using NextAuth.js
  const session = await getServerSession(req, res, authOptions);

  if (!session) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const { method } = req;

  switch (method) {
    case 'GET':
      try {
        // Get user profile from database
        const userProfile = await getUserProfile(session.user.id);
        res.status(200).json(userProfile);
      } catch (error) {
        res.status(500).json({ error: 'Failed to fetch profile' });
      }
      break;

    case 'PUT':
      try {
        const updates = req.body;
        const updatedProfile = await updateUserProfile(session.user.id, updates);
        res.status(200).json(updatedProfile);
      } catch (error) {
        res.status(500).json({ error: 'Failed to update profile' });
      }
      break;

    default:
      res.setHeader('Allow', ['GET', 'PUT']);
      res.status(405).end(`Method ${method} Not Allowed`);
  }
}

// Mock database functions
async function getUserProfile(userId) {
  return { id: userId, name: 'John Doe', email: 'john@example.com' };
}

async function updateUserProfile(userId, updates) {
  return { id: userId, ...updates, updatedAt: new Date() };
}

Rate Limiting Middleware

jsx

// middleware/rate-limit.js
import { NextResponse } from 'next/server';

// Simple in-memory store (use Redis in production)
const requestCounts = new Map();

export function rateLimitMiddleware(request) {
  const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown';
  const now = Date.now();
  const windowMs = 15 * 60 * 1000; // 15 minutes
  const maxRequests = 100; // 100 requests per window

  // Clean old entries
  cleanupOldEntries(now, windowMs);

  const key = `${ip}:${request.nextUrl.pathname}`;
  const requests = requestCounts.get(key) || [];

  // Remove requests outside the current window
  const recentRequests = requests.filter(time => now - time < windowMs);

  if (recentRequests.length >= maxRequests) {
    return NextResponse.json(
      { error: 'Too many requests' },
      { status: 429 }
    );
  }

  // Add current request
  recentRequests.push(now);
  requestCounts.set(key, recentRequests);

  // Add rate limit headers to response
  const response = NextResponse.next();
  response.headers.set('X-RateLimit-Limit', maxRequests.toString());
  response.headers.set('X-RateLimit-Remaining', (maxRequests - recentRequests.length).toString());
  
  return response;
}

function cleanupOldEntries(now, windowMs) {
  for (const [key, requests] of requestCounts.entries()) {
    const recentRequests = requests.filter(time => now - time < windowMs);
    if (recentRequests.length === 0) {
      requestCounts.delete(key);
    } else {
      requestCounts.set(key, recentRequests);
    }
  }
}

Best Practices

1. Error Handling

jsx

// lib/api-error.js
export class APIError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.statusCode = statusCode;
    this.name = 'APIError';
  }
}

// pages/api/with-error-handling.js
import { APIError } from '../../lib/api-error';

export default async function handler(req, res) {
  try {
    if (req.method !== 'GET') {
      throw new APIError('Method not allowed', 405);
    }

    const data = await fetchData();
    
    if (!data) {
      throw new APIError('Data not found', 404);
    }

    res.status(200).json(data);
  } catch (error) {
    if (error instanceof APIError) {
      res.status(error.statusCode).json({ error: error.message });
    } else {
      console.error('Unexpected error:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
}

2. Environment Variables

jsx

// pages/api/config.js
export default function handler(req, res) {
  // Server-side environment variables
  const config = {
    apiUrl: process.env.API_URL,
    databaseUrl: process.env.DATABASE_URL,
    secretKey: process.env.SECRET_KEY,
    // Never expose sensitive data to client
  };

  res.status(200).json({
    apiUrl: config.apiUrl,
    // Only send non-sensitive config to client
  });
}

3. Response Helpers

jsx

// lib/api-helpers.js
export function successResponse(res, data, statusCode = 200) {
  return res.status(statusCode).json({
    success: true,
    data,
    timestamp: new Date().toISOString()
  });
}

export function errorResponse(res, message, statusCode = 500) {
  return res.status(statusCode).json({
    success: false,
    error: message,
    timestamp: new Date().toISOString()
  });
}

// Usage in API route
import { successResponse, errorResponse } from '../../lib/api-helpers';

export default function handler(req, res) {
  try {
    const data = { message: 'Success' };
    return successResponse(res, data, 201);
  } catch (error) {
    return errorResponse(res, error.message, 400);
  }
}

Next.js API Routes provide a powerful way to build full-stack applications with a unified technology stack, while middleware offers robust solutions for cross-cutting concerns like authentication, logging, and security.

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?