Logo
Published on

How to protect your API routes in Next.js App Router

Authors
tailwind-nextjs-banner

Recently, we saw what happened to Shipfa.st when their API routes weren't properly protected. Let's make sure your Next.js application doesn't suffer the same fate. In this guide, I'll show you four simple strategies to secure your API routes in Next.js App Router. It's really simple, so please just do it instead of ignoring something critical.

Why API Security Matters

Your API routes are the gateway to your application's data and functionality. Without proper protection, you're essentially leaving your front door wide open. Bad actors can:

  • Abuse your API endpoints with unlimited requests
  • Access sensitive data without authorization
  • Perform actions on behalf of other users
  • Rack up your server costs with automated attacks

1. Authentication Headers:

The simplest way to protect your API routes is by requiring an authentication token. Here's how to implement it:

// app/api/protected/route.ts
export async function GET(request: Request) {
  const authHeader = request.headers.get('authorization')
  
  if (authHeader !== `Bearer ${process.env.API_SECRET}`) {
    return new Response('Unauthorized', { status: 401 })
  }
  
  return Response.json({ 
    message: 'Welcome to the protected data!'
  })
}

To use this endpoint, clients need to include the correct authentication header:

const response = await fetch('/api/protected', {
  headers: {
    'Authorization': `Bearer ${process.env.API_SECRET}`
  }
})

This approach works by checking for a Bearer token in the request headers. The token should match exactly with your secret key stored in environment variables. Think of it like a special password that only your application knows. When a request comes in without this token or with an incorrect one, it's immediately rejected with a 401 Unauthorized response.

2. Rate Limiting:

Rate limiting is crucial for preventing abuse and maintaining performance. Rate limiting tracks how many requests come from a single IP address within a time window. In this example, we're allowing 10 requests per minute per IP address. Here's how it works:

  • We use Redis as a fast, in-memory database to track requests
  • Each request increments a counter for that IP
  • The counter automatically expires after 60 seconds
  • If someone exceeds 10 requests in that window, they get locked out temporarily
import { Redis } from '@upstash/redis'

export async function POST(request: Request) {
  const ip = request.headers.get('x-forwarded-for')
  const redis = Redis.fromEnv()
  
  // Increment request count for this IP
  const requests = await redis.incr(`ratelimit:${ip}`)
  
  // Set expiry for 1 minute if this is the first request
  if (requests === 1) {
    await redis.expire(`ratelimit:${ip}`, 60)
  }
  
  // Reject if too many requests
  if (requests > 10) {
    return new Response('Too many requests', { 
      status: 429,
      headers: {
        'Retry-After': '60'
      }
    })
  }
  
  return Response.json({ success: true })
}

3. Session-Based Protection with an Auth Provider:

While API keys are great for service-to-service communication, user sessions are better for browser-based access. I'll give an example using NextAuth.

// any route like: /api/dontFuckWithSecurity/route.ts
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/app/api/auth/[...nextauth]/route"

export async function GET() {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    return new Response('Please sign in', { 
      status: 401 
    })
  }
  
  return Response.json({
    user: session.user,
    data: 'Your protected content'
  })
}

4. Global Protection with Middleware:

Middleware allows you to protect multiple routes with a single piece of code. The beauty of middleware is that it runs before your route handlers, meaning:

  • You can protect multiple routes with a single piece of code
  • Unauthorized requests are rejected early, saving server resources
  • You can apply different rules based on the route pattern
  • It's perfect for global authentication checks
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/')) {
    const token = request.headers.get('authorization')
    
    if (!isValidToken(token)) {
      return new NextResponse(
        JSON.stringify({ error: 'unauthorized' }),
        { 
          status: 401,
          headers: {
            'Content-Type': 'application/json'
          }
        }
      )
    }
  }
}

export const config = {
  matcher: '/api/:path*'
}

Best Practices for API Security

Beyond these strategies, here are some additional tips to keep your API routes secure:

1. Input Validation

Always validate incoming data using a schema validation library like Zod:

import { z } from 'zod'

const userSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(2)
})

export async function POST(request: Request) {
  try {
    const body = await request.json()
    const validatedData = userSchema.parse(body)
  } catch (error) {
    return new Response('Invalid input', { status: 400 })
  }
}

2. Environment Variables

Keep sensitive data in environment variables:

# .env.local
API_SECRET=your_secret_key
ALLOWED_ORIGINS=https://yourdomain.com

3. CORS Configuration

For APIs accessed from different domains:

export async function GET(request: Request) {
  return new Response('Data', {
    headers: {
      'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN,
      'Access-Control-Allow-Methods': 'GET, POST',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization'
    }
  })
}

Please keep this in mind

API security isn't optional – it's a crucial part of building production-ready applications. You can never dismiss it by calling it unnecessary in the name of shipping faster. By implementing these simple strategies, you can protect your routes from unauthorized access and abuse. Don't wait for a security incident to start thinking about API protection.

Additional Resources