How to Create Api Routes in Nextjs

How to Create API Routes in Next.js Next.js has revolutionized the way developers build full-stack JavaScript applications by seamlessly blending server-side rendering, static site generation, and API functionality within a single framework. One of its most powerful yet underutilized features is the built-in API route system. With API routes, you can create backend endpoints directly inside your N

Oct 30, 2025 - 13:23
Oct 30, 2025 - 13:23
 1

How to Create API Routes in Next.js

Next.js has revolutionized the way developers build full-stack JavaScript applications by seamlessly blending server-side rendering, static site generation, and API functionality within a single framework. One of its most powerful yet underutilized features is the built-in API route system. With API routes, you can create backend endpoints directly inside your Next.js application—eliminating the need for a separate server or external service like Express.js for simple to moderately complex backend logic.

This tutorial provides a comprehensive, step-by-step guide on how to create API routes in Next.js. Whether you're building a small personal project or scaling a production-grade application, understanding API routes is essential for modern web development. You’ll learn how to structure endpoints, handle HTTP methods, manage middleware, integrate databases, secure routes, and follow industry best practices—all within the familiar Next.js directory structure.

By the end of this guide, you’ll have the confidence to implement robust, scalable, and maintainable API routes that enhance your Next.js applications with dynamic backend capabilities—without leaving the framework.

Step-by-Step Guide

Understanding API Routes in Next.js

Next.js API routes are server-side functions that live inside the pages/api directory (in Next.js 12 and earlier) or app/api directory (in Next.js 13+ with the App Router). These routes are automatically mapped to URLs based on their file path. For example, a file named pages/api/hello.js becomes accessible at /api/hello.

Each API route is an HTTP handler function that receives two arguments: req (the HTTP request object) and res (the HTTP response object). You can respond with JSON, HTML, files, or any other HTTP-compatible format.

Unlike traditional Node.js servers, Next.js API routes are serverless by default when deployed on Vercel. This means they scale automatically and incur no server maintenance costs. Even when self-hosted, they benefit from Next.js’s optimized runtime and hot-reloading during development.

Setting Up a New Next.js Project

Before creating API routes, ensure you have a Next.js project ready. If you don’t have one, create it using the official CLI:

npx create-next-app@latest my-nextjs-app

cd my-nextjs-app

During setup, choose the default options unless you have specific requirements. Once the project is created, navigate to the pages directory. In Next.js 12 and earlier, API routes are located here. In Next.js 13 and later, if you're using the App Router, you'll need to create an app/api directory instead.

For this guide, we’ll assume you’re using Next.js 13+ with the App Router. If you're on an older version, the structure is similar but located under pages/api.

Creating Your First API Route

To create your first API route, navigate to your project’s root directory and create the following folder structure:

app/

└── api/

└── hello/

└── route.js

Inside route.js, add the following code:

import { NextResponse } from 'next/server';

export async function GET(request) {

return NextResponse.json({ message: 'Hello from Next.js API Route!' });

}

This defines a simple GET endpoint. To test it, start your development server:

npm run dev

Then visit http://localhost:3000/api/hello. You should see the JSON response:

{ "message": "Hello from Next.js API Route!" }

Notice that we used NextResponse instead of the traditional res object. This is because the App Router uses the modern Fetch API pattern, which is more aligned with modern JavaScript standards and serverless environments.

Handling Different HTTP Methods

API routes can respond to different HTTP methods: GET, POST, PUT, DELETE, PATCH, HEAD, and OPTIONS. In the App Router, you define these as separate exported functions.

Let’s create a more advanced endpoint that handles multiple methods. Create a new route at app/api/users/route.js:

import { NextResponse } from 'next/server';

// Mock user data (in production, use a database)

const users = [

{ id: 1, name: 'Alice', email: 'alice@example.com' },

{ id: 2, name: 'Bob', email: 'bob@example.com' },

];

export async function GET() {

return NextResponse.json(users);

}

export async function POST(request) {

const body = await request.json();

const newUser = {

id: users.length + 1,

name: body.name,

email: body.email,

};

users.push(newUser);

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

}

export async function DELETE(request) {

const url = new URL(request.url);

const id = url.searchParams.get('id');

if (!id) {

return NextResponse.json({ error: 'ID parameter is required' }, { status: 400 });

}

const index = users.findIndex(user => user.id === parseInt(id));

if (index === -1) {

return NextResponse.json({ error: 'User not found' }, { status: 404 });

}

users.splice(index, 1);

return NextResponse.json({ message: 'User deleted' });

}

Now you can:

  • GET /api/users → returns all users
  • POST /api/users with JSON body → adds a new user
  • DELETE /api/users?id=1 → removes a user by ID

Each method is independent, making your code modular and easier to test. You can also use PUT or PATCH for partial updates.

Working with Request and Response Objects

In the App Router, the request object is a standard Request object from the Fetch API. You can extract headers, URL parameters, query strings, and request bodies easily.

For example, to access query parameters:

export async function GET(request) {

const { searchParams } = new URL(request.url);

const category = searchParams.get('category');

const limit = searchParams.get('limit') || 10;

// Filter data based on category

const filteredData = data.filter(item => item.category === category);

return NextResponse.json({

category,

limit: parseInt(limit),

results: filteredData.slice(0, parseInt(limit))

});

}

To access headers:

export async function POST(request) {

const apiKey = request.headers.get('X-API-Key');

if (!apiKey || apiKey !== process.env.API_KEY) {

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

}

const body = await request.json();

// Process data...

return NextResponse.json({ success: true });

}

Response objects are created using NextResponse.json(), NextResponse.text(), or NextResponse.redirect(). You can also set custom headers:

const response = NextResponse.json({ data: 'example' });

response.headers.set('Cache-Control', 'no-cache');

return response;

Using Environment Variables

API routes often need access to sensitive data like database URLs, API keys, or secrets. Next.js supports environment variables via a .env.local file.

Create .env.local in your project root:

API_KEY=your-secret-key-here

DATABASE_URL=mongodb://localhost:27017/myapp

Access them in your API route using process.env:

export async function POST(request) {

const apiKey = request.headers.get('X-API-Key');

if (apiKey !== process.env.API_KEY) {

return NextResponse.json({ error: 'Invalid API key' }, { status: 401 });

}

// Use DATABASE_URL to connect to MongoDB, etc.

// ...

}

Important: Only variables prefixed with NEXT_PUBLIC_ are exposed to the browser. All others remain server-side only, making them safe for secrets.

Integrating with Databases

API routes are ideal for connecting to databases. You can use any Node.js-compatible database driver: MongoDB (via Mongoose), PostgreSQL (via pg), MySQL (via mysql2), or even SQLite.

Let’s connect to MongoDB using Mongoose. First, install the required packages:

npm install mongoose

Create a utility file to manage the database connection at lib/dbConnect.js:

import { connect } from 'mongoose';

const MONGODB_URI = process.env.MONGODB_URI;

if (!MONGODB_URI) {

throw new Error('Please define the MONGODB_URI environment variable inside .env.local');

}

let cached = global.mongoose;

if (!cached) {

cached = global.mongoose = { conn: null, promise: null };

}

async function dbConnect() {

if (cached.conn) {

return cached.conn;

}

if (!cached.promise) {

const opts = {

bufferCommands: false,

};

cached.promise = connect(MONGODB_URI, opts).then((mongoose) => {

return mongoose;

});

}

try {

cached.conn = await cached.promise;

} catch (e) {

cached.promise = null;

throw e;

}

return cached.conn;

}

export default dbConnect;

Now use it in your API route at app/api/posts/route.js:

import { NextResponse } from 'next/server';

import dbConnect from '@/lib/dbConnect';

import Post from '@/models/Post'; // Your Mongoose model

export async function GET() {

await dbConnect();

const posts = await Post.find().sort({ createdAt: -1 });

return NextResponse.json(posts);

}

export async function POST(request) {

await dbConnect();

const body = await request.json();

const post = new Post(body);

await post.save();

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

}

Make sure your Mongoose model (models/Post.js) is properly defined:

import { Schema, model } from 'mongoose';

const PostSchema = new Schema({

title: { type: String, required: true },

content: { type: String, required: true },

author: { type: String, required: true },

}, { timestamps: true });

export default model('Post', PostSchema);

This approach ensures your database connection is reused across requests, reducing latency and avoiding connection leaks.

Handling Errors Gracefully

Always handle errors in API routes to prevent unhandled rejections and provide meaningful responses to clients.

Wrap your logic in try-catch blocks:

export async function POST(request) {

try {

const body = await request.json();

const user = await User.create(body);

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

} catch (error) {

console.error('Error creating user:', error);

if (error.name === 'ValidationError') {

return NextResponse.json(

{ error: 'Validation failed', details: error.message },

{ status: 400 }

);

}

if (error.code === 11000) {

return NextResponse.json(

{ error: 'Email already exists' },

{ status: 409 }

);

}

return NextResponse.json(

{ error: 'Internal server error' },

{ status: 500 }

);

}

}

Use appropriate HTTP status codes:

  • 200 – OK (successful GET)
  • 201 – Created (successful POST)
  • 400 – Bad Request (invalid input)
  • 401 – Unauthorized (missing or invalid auth)
  • 403 – Forbidden (authenticated but not authorized)
  • 404 – Not Found
  • 500 – Internal Server Error

Logging errors is also critical. Use a logging library like winston or pino in production, or at minimum log to the console for development.

Using Middleware for Authentication and Logging

Next.js 13+ supports middleware that can run before API routes (and pages). Create a middleware.js file in your root directory:

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request) {

console.log(Request to ${request.url});

}

export const config = {

matcher: ['/api/:path*'], // Apply only to API routes

};

This logs every API request. You can also use middleware for authentication:

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request) {

const token = request.headers.get('Authorization');

if (!token || token !== Bearer ${process.env.API_TOKEN}) {

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

}

}

export const config = {

matcher: ['/api/protected/:path*'],

};

Now any route under /api/protected will require a valid token. This keeps authentication logic centralized and reusable across multiple routes.

Best Practices

Organize API Routes by Resource

Structure your API routes logically. Group related endpoints under the same directory. For example:

app/

└── api/

├── users/ │ ├── route.js

GET, POST

│ └── [id]/ │ └── route.js

GET, PUT, DELETE by ID

├── posts/

│ ├── route.js

│ └── [id]/

│ └── route.js

└── auth/

├── login/

│ └── route.js

└── register/

└── route.js

This makes your codebase scalable and intuitive. It also mirrors RESTful conventions and improves team collaboration.

Use TypeScript for Type Safety

If you’re using TypeScript (highly recommended), define interfaces for your request and response payloads:

interface CreateUserRequest {

name: string;

email: string;

}

export async function POST(request: Request) {

const body: CreateUserRequest = await request.json();

if (!body.name || !body.email) {

return NextResponse.json(

{ error: 'Name and email are required' },

{ status: 400 }

);

}

const user = await User.create(body);

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

}

TypeScript catches errors at build time and improves developer experience with IntelliSense and autocompletion.

Validate Input with Zod or Joi

Never trust client input. Use a schema validation library like Zod to validate request bodies:

import { z } from 'zod';

const createUserSchema = z.object({

name: z.string().min(2),

email: z.string().email(),

});

export async function POST(request: Request) {

const body = await request.json();

const result = createUserSchema.safeParse(body);

if (!result.success) {

return NextResponse.json(

{ error: 'Invalid input', details: result.error.errors },

{ status: 400 }

);

}

const user = await User.create(result.data);

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

}

Zod is lightweight, type-safe, and integrates perfectly with TypeScript. It’s the preferred choice in the Next.js ecosystem.

Rate Limiting

Protect your API from abuse by implementing rate limiting. Use libraries like rate-limiter-flexible or next-rate-limiter:

import { RateLimiterMemory } from 'rate-limiter-flexible';

const rateLimiter = new RateLimiterMemory({

points: 10, // 10 requests

duration: 60, // per 60 seconds

});

export async function POST(request: Request) {

const ip = request.headers.get('x-forwarded-for') || request.remoteAddress;

try {

await rateLimiter.consume(ip || 'unknown');

} catch (rateLimiterRes) {

return NextResponse.json(

{ error: 'Too many requests' },

{ status: 429 }

);

}

// Proceed with logic...

}

Rate limiting prevents DDoS attacks and ensures fair usage.

Cache Responses Strategically

Use caching to improve performance. For read-heavy endpoints, set Cache-Control headers:

export async function GET() {

const posts = await Post.find().limit(10);

const response = NextResponse.json(posts);

response.headers.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=59');

return response;

}

This tells CDNs and browsers to cache the response for an hour, reducing server load and improving latency.

Secure API Routes

Always enforce authentication for sensitive endpoints. Use JWT, OAuth, or session-based auth. Avoid exposing internal data via API routes. Use environment variables for secrets. Sanitize all inputs. Never log sensitive data.

Test Your API Routes

Write unit and integration tests using Jest or Vitest:

import { describe, it, expect } from 'vitest';

import { createRequest } from 'node:test';

describe('API /api/users', () => {

it('returns list of users on GET', async () => {

const req = new Request('http://localhost:3000/api/users');

const res = await GET(req);

const data = await res.json();

expect(data.length).toBeGreaterThan(0);

});

});

Testing ensures reliability and prevents regressions.

Tools and Resources

Recommended Libraries

  • Zod – Type-safe schema validation
  • Mongoose – MongoDB ODM
  • Prisma – Modern ORM for SQL and NoSQL databases
  • Rate-Limiter-Flexible – Rate limiting
  • Pino – Fast logging
  • Winston – Flexible logging
  • Postman or Insomnia – API testing tools
  • Swagger UI – Auto-generate API documentation

Deployment Platforms

Next.js API routes deploy seamlessly on:

  • Vercel – Official platform with automatic scaling and serverless functions
  • Netlify – Supports API routes via Netlify Functions
  • Render – Full Node.js environment
  • Docker + Nginx – Self-hosted option for full control

Vercel is the most seamless option for Next.js apps. API routes become serverless functions automatically, with no configuration needed.

Documentation and Learning Resources

Real Examples

Example 1: Email Subscription Endpoint

A common use case is a newsletter signup form. Here’s how to build it securely:

// app/api/subscribe/route.js

import { NextResponse } from 'next/server';

import { z } from 'zod';

import { sendEmail } from '@/lib/email';

const subscribeSchema = z.object({

email: z.string().email(),

});

export async function POST(request) {

const body = await request.json();

const result = subscribeSchema.safeParse(body);

if (!result.success) {

return NextResponse.json(

{ error: 'Invalid email' },

{ status: 400 }

);

}

try {

await sendEmail(result.data.email, 'Welcome!', 'Thank you for subscribing.');

return NextResponse.json({ success: true });

} catch (error) {

console.error('Failed to send email:', error);

return NextResponse.json(

{ error: 'Failed to subscribe. Try again later.' },

{ status: 500 }

);

}

}

This route validates input, sends an email via a service like Resend or Nodemailer, and returns appropriate responses.

Example 2: Webhook Handler for Stripe

When integrating payment systems like Stripe, you need to handle webhooks:

// app/api/webhooks/stripe/route.js

import { NextResponse } from 'next/server';

import { buffer } from 'micro';

import { Webhook } from 'stripe';

const stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET;

export const config = {

api: {

bodyParser: false,

},

};

export async function POST(request) {

const buf = await buffer(request);

const sig = request.headers.get('stripe-signature');

let event;

try {

event = new Webhook(stripeWebhookSecret).constructEvent(

buf.toString(),

sig,

stripeWebhookSecret

);

} catch (err) {

return NextResponse.json({ error: 'Webhook signature invalid' }, { status: 400 });

}

// Handle event types

if (event.type === 'payment_intent.succeeded') {

const paymentIntent = event.data.object;

// Update database, send confirmation email, etc.

}

return NextResponse.json({ received: true });

}

This example shows how to handle raw HTTP bodies (required for Stripe), validate signatures, and process events securely.

Example 3: File Upload Endpoint

Uploading files via API is common. Use multer or form-data to handle multipart requests:

// app/api/upload/route.js

import { NextResponse } from 'next/server';

import { parse } from 'path';

export async function POST(request) {

const formData = await request.formData();

const file = formData.get('file');

if (!file || !(file instanceof File)) {

return NextResponse.json({ error: 'No file provided' }, { status: 400 });

}

const bytes = await file.arrayBuffer();

const buffer = Buffer.from(bytes);

const fileName = ${Date.now()}-${file.name};

const filePath = public/uploads/${fileName};

// Save file to public folder (for static serving)

await Bun.write(filePath, buffer);

return NextResponse.json({

url: /uploads/${fileName},

name: file.name,

});

}

For production, upload to cloud storage like AWS S3 or Cloudinary instead of saving locally.

FAQs

Can I use API routes in production?

Yes. Next.js API routes are production-ready. When deployed on Vercel, they become serverless functions that scale automatically. On self-hosted servers, they run as part of the Next.js server process and handle concurrent requests efficiently.

Are API routes slower than a dedicated Node.js server?

For most use cases, no. API routes benefit from Next.js’s optimized runtime and cold-start optimizations on Vercel. For high-throughput, low-latency applications (e.g., real-time gaming or financial trading), a dedicated Node.js server with Express might offer marginal performance gains—but at the cost of complexity and maintenance.

Can I use API routes with authentication systems like NextAuth?

Absolutely. NextAuth (now Auth.js) integrates seamlessly with API routes. You can use getServerSession to protect routes or validate JWT tokens within your API handlers.

How do I handle large file uploads?

For large files (over 5MB), avoid buffering the entire file in memory. Use streaming or upload directly to cloud storage (AWS S3, Cloudinary, etc.). Use libraries like multer-s3 or uploadthing for scalable file handling.

Do API routes support WebSockets?

Not natively. API routes are designed for HTTP request-response cycles. For real-time communication, use a dedicated WebSocket server (e.g., Socket.IO) or services like Pusher or Supabase Realtime.

Can I use API routes with GraphQL?

Yes. You can create a single /api/graphql route that acts as a GraphQL endpoint using libraries like graphql-yoga or apollo-server-micro.

Why is my API route returning 404?

Common causes:

  • File is not in the correct directory (app/api/ or pages/api/)
  • File name doesn’t match the URL path (e.g., route.js for /api/user)
  • Using the wrong HTTP method (e.g., POST instead of GET)
  • Restarting the dev server after adding a new route (required)

How do I test API routes locally?

Use tools like Postman, Insomnia, or cURL:

curl -X POST http://localhost:3000/api/users \

-H "Content-Type: application/json" \

-d '{"name":"John","email":"john@example.com"}'

Or write automated tests using Vitest or Jest with next/test or node-fetch.

Conclusion

Creating API routes in Next.js is a powerful way to build full-stack applications without leaving the framework. Whether you’re building a simple contact form or a complex SaaS product with user authentication, database interactions, and third-party integrations, Next.js API routes provide a clean, scalable, and maintainable solution.

In this guide, we’ve walked through everything from setting up your first endpoint to integrating databases, validating inputs, securing routes, and deploying to production. We’ve explored best practices like using TypeScript, Zod for validation, middleware for authentication, and rate limiting for security. Real-world examples showed how to handle email subscriptions, Stripe webhooks, and file uploads—all within the Next.js ecosystem.

By following these patterns, you’ll write code that’s not only functional but also professional, secure, and scalable. API routes in Next.js eliminate the friction of managing separate backend services, allowing you to focus on building features—not infrastructure.

As you continue to develop with Next.js, remember that the goal is simplicity and developer experience. Let the framework handle the heavy lifting while you focus on delivering value to your users. With API routes, you’re no longer limited to frontend-only applications—you’re building full-stack applications with the speed and elegance that only Next.js can deliver.