How to Create Api Routes in Nextjs

How to Create API Routes in Next.js Next.js has revolutionized the way developers build modern web applications by combining server-side rendering, static site generation, and API functionality into a single, cohesive framework. One of its most powerful yet underutilized features is the ability to create API routes directly within your Next.js project. These API routes allow you to build full-stac

Nov 10, 2025 - 08:40
Nov 10, 2025 - 08:40
 1

How to Create API Routes in Next.js

Next.js has revolutionized the way developers build modern web applications by combining server-side rendering, static site generation, and API functionality into a single, cohesive framework. One of its most powerful yet underutilized features is the ability to create API routes directly within your Next.js project. These API routes allow you to build full-stack applications without needing a separate backend server, making development faster, simpler, and more cost-effective.

API routes in Next.js are file-based endpoints that automatically become accessible via HTTP requests. By simply creating a file inside the pages/api directory (in Next.js 12 and earlier) or the app/api directory (in Next.js 13+ with the App Router), you can expose RESTful endpoints that handle GET, POST, PUT, DELETE, and other HTTP methods. This eliminates the need to manage a separate Express.js, NestJS, or Node.js server, reducing complexity and deployment overhead.

Whether you're building a headless CMS, integrating third-party services, handling form submissions, or creating authentication endpoints, Next.js API routes provide a seamless way to connect your frontend with backend logicall within the same codebase. This tutorial will guide you through every step of creating, organizing, securing, and optimizing API routes in Next.js, from beginner fundamentals to advanced best practices. By the end, youll have a comprehensive understanding of how to leverage API routes to build scalable, production-ready applications.

Step-by-Step Guide

Setting Up Your Next.js Project

Before you begin creating API routes, ensure you have a Next.js project ready. If you havent created one yet, open your terminal and run the following command:

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

Follow the prompts to configure your project. Choose options like TypeScript, ESLint, Tailwind CSS, or App Router based on your preferences. Once the setup is complete, navigate into your project folder:

cd my-nextjs-app

Next.js 13 introduced the App Router, which restructures the project layout. If you selected the App Router during setup, your API routes will be placed in the app/api directory. If you opted for the Pages Router (used in Next.js 12 and earlier), your API routes go in pages/api. For this guide, well focus on the App Router, as its the future of Next.js development.

Creating Your First API Route

To create your first API route, navigate to the app directory and create a new folder named api. Inside app/api, create a subfolder named hello, and within that, create a file called route.js (or route.ts if using TypeScript).

Open app/api/hello/route.js and add the following code:

export async function GET(request) {

return new Response(JSON.stringify({ message: 'Hello from Next.js API!' }), {

status: 200,

headers: {

'Content-Type': 'application/json',

},

});

}

This simple endpoint defines a GET handler that returns a JSON response. The function must be exported as GET, POST, PUT, etc., corresponding to the HTTP method you wish to support. Next.js automatically maps this file to the route /api/hello.

Start your development server:

npm run dev

Visit http://localhost:3000/api/hello in your browser. You should see the JSON response:

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

Thats ityouve created your first API route in Next.js!

Handling Different HTTP Methods

API routes can respond to multiple HTTP methods. For example, a resource like a user profile might support GET (retrieve), POST (create), PUT (update), and DELETE (remove). Heres how to handle multiple methods in a single route file:

export async function GET(request) {

return new Response(JSON.stringify({ message: 'Fetching user data...' }), {

status: 200,

headers: { 'Content-Type': 'application/json' },

});

}

export async function POST(request) {

const body = await request.json();

return new Response(JSON.stringify({ message: 'User created', data: body }), {

status: 201,

headers: { 'Content-Type': 'application/json' },

});

}

export async function PUT(request) {

const body = await request.json();

return new Response(JSON.stringify({ message: 'User updated', data: body }), {

status: 200,

headers: { 'Content-Type': 'application/json' },

});

}

export async function DELETE(request) {

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

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

if (!id) {

return new Response(JSON.stringify({ error: 'ID is required' }), {

status: 400,

headers: { 'Content-Type': 'application/json' },

});

}

return new Response(JSON.stringify({ message: User ${id} deleted }), {

status: 200,

headers: { 'Content-Type': 'application/json' },

});

}

Each method is defined as a separate async function. When a request is made to /api/hello with a specific HTTP method, Next.js automatically invokes the corresponding handler.

To test this, use a tool like cURL, Postman, or your browsers developer tools:

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

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

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

Youll receive a response indicating the user was created.

Accessing Request Data

API routes in Next.js provide access to the incoming request object via the request parameter. This object contains headers, query parameters, cookies, and the request body.

To extract JSON data from a POST or PUT request, use await request.json():

export async function POST(request) {

const data = await request.json();

console.log(data); // { name: 'Alice', age: 30 }

return new Response(JSON.stringify({ received: data }), {

status: 200,

headers: { 'Content-Type': 'application/json' },

});

}

To read URL query parameters, use the built-in URL constructor:

export async function GET(request) {

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

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

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

if (!name) {

return new Response(JSON.stringify({ error: 'Name parameter is required' }), {

status: 400,

headers: { 'Content-Type': 'application/json' },

});

}

return new Response(JSON.stringify({ message: Hello ${name}, age ${age || 'unknown'} }), {

status: 200,

headers: { 'Content-Type': 'application/json' },

});

}

Test this route with: http://localhost:3000/api/hello?name=Sarah&age=25

Working with Form Data and Files

When handling form submissions, especially those with file uploads, youll need to parse multipart/form-data. Next.js provides the FormData API for this purpose.

export async function POST(request) {

const formData = await request.formData();

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

const avatar = formData.get('avatar'); // File object

if (!name) {

return new Response(JSON.stringify({ error: 'Name is required' }), {

status: 400,

headers: { 'Content-Type': 'application/json' },

});

}

// Handle file upload (in production, use cloud storage like AWS S3 or Cloudinary)

if (avatar && avatar instanceof File) {

console.log('File received:', avatar.name, avatar.type, avatar.size);

// Save file to disk or upload to cloud

}

return new Response(JSON.stringify({ message: 'Form submitted', name }), {

status: 201,

headers: { 'Content-Type': 'application/json' },

});

}

For production applications, avoid saving files directly to the filesystem. Instead, integrate with cloud storage services like Cloudinary, AWS S3, or Firebase Storage.

Using Environment Variables

Never hardcode sensitive information like API keys, database URLs, or secrets in your route files. Instead, use environment variables.

Create a .env.local file in your project root:

NEXT_PUBLIC_API_KEY=your-public-key

DATABASE_URL=postgresql://user:pass@localhost:5432/mydb

SECRET_TOKEN=your-secret-token

Access them in your API route:

export async function GET(request) {

const apiKey = process.env.NEXT_PUBLIC_API_KEY;

const secretToken = process.env.SECRET_TOKEN;

// Use apiKey for public-facing integrations

// Use secretToken for internal authentication

return new Response(JSON.stringify({ apiKey }), {

status: 200,

headers: { 'Content-Type': 'application/json' },

});

}

Note: Variables prefixed with NEXT_PUBLIC_ are exposed to the browser. All others remain server-side only.

Connecting to a Database

API routes are perfect for connecting to databases. Lets connect to PostgreSQL using Prisma, a popular ORM.

First, install Prisma:

npm install @prisma/client prisma

Initialize Prisma:

npx prisma init

Configure your database URL in prisma/.env and define a schema in prisma/schema.prisma:

model User {

id Int @id @default(autoincrement())

name String

email String @unique

}

Generate the Prisma client:

npx prisma generate

Now, create a database connection in your API route:

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

const prisma = new PrismaClient();

export async function POST(request) {

const body = await request.json();

const { name, email } = body;

try {

const user = await prisma.user.create({

data: { name, email },

});

return new Response(JSON.stringify(user), {

status: 201,

headers: { 'Content-Type': 'application/json' },

});

} catch (error) {

return new Response(JSON.stringify({ error: error.message }), {

status: 500,

headers: { 'Content-Type': 'application/json' },

});

} finally {

await prisma.$disconnect();

}

}

Prisma ensures type safety and simplifies database interactions. Always close the connection with $disconnect() to prevent memory leaks.

Using Middleware for Authentication and Logging

Next.js 13+ supports middleware that can intercept requests before they reach your API routes. Create a middleware.js file in your project root:

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

export function middleware(request) {

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

}

export const config = {

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

};

This logs every API request. You can extend this to validate tokens, block IPs, or redirect unauthorized users.

For authentication, you might check for a valid JWT:

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

export function middleware(request) {

const token = request.headers.get('Authorization')?.split(' ')[1];

if (token !== process.env.SECRET_TOKEN) {

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

}

}

export const config = {

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

};

Now, any request to /api/protected/* must include a valid token in the Authorization header.

Best Practices

Organize API Routes Logically

As your application grows, avoid dumping all API routes into a single app/api folder. Instead, organize them by domain or feature:

  • app/api/auth/login/route.js
  • app/api/auth/register/route.js
  • app/api/users/[id]/route.js
  • app/api/products/route.js
  • app/api/products/[id]/route.js
  • app/api/webhooks/stripe/route.js

This structure mirrors RESTful conventions and improves maintainability. Use dynamic routes (e.g., [id]) for resource-specific endpoints.

Use Proper HTTP Status Codes

Always return appropriate HTTP status codes to communicate the outcome of a request:

  • 200 OK Successful GET request
  • 201 Created Resource successfully created (POST)
  • 204 No Content Successful DELETE or UPDATE with no response body
  • 400 Bad Request Invalid input or missing parameters
  • 401 Unauthorized Authentication required
  • 403 Forbidden Authenticated but insufficient permissions
  • 404 Not Found Resource doesnt exist
  • 500 Internal Server Error Unexpected server error

Consistent status codes make your API predictable and easier for clients to consume.

Validate and Sanitize Input

Never trust user input. Always validate data before processing:

import { z } from 'zod';

const userSchema = z.object({

name: z.string().min(1).max(50),

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

age: z.number().int().min(0).max(120).optional(),

});

export async function POST(request) {

const body = await request.json();

const result = userSchema.safeParse(body);

if (!result.success) {

return new Response(

JSON.stringify({ error: 'Invalid input', details: result.error.errors }),

{ status: 400, headers: { 'Content-Type': 'application/json' } }

);

}

// Proceed with validated data

return new Response(JSON.stringify({ message: 'Valid user', data: result.data }), {

status: 201,

headers: { 'Content-Type': 'application/json' },

});

}

Use libraries like Zod, Joi, or Yup for schema validation. They provide type inference and detailed error messages.

Implement Rate Limiting

To prevent abuse and DDoS attacks, implement rate limiting on public endpoints. Use libraries like rate-limiter-flexible:

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

const rateLimiter = new RateLimiterMemory({

points: 10, // 10 requests

duration: 60, // per 60 seconds

});

export async function POST(request) {

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

try {

await rateLimiter.consume(ip);

} catch (rej) {

return new Response(JSON.stringify({ error: 'Too many requests' }), {

status: 429,

headers: { 'Content-Type': 'application/json' },

});

}

// Handle request

}

For production, use Redis-based rate limiting for distributed applications.

Secure Your API Routes

Protect sensitive routes with authentication:

  • Use JWT tokens stored in HTTP-only cookies
  • Validate token signatures and expiration
  • Implement refresh token rotation
  • Use HTTPS in production
  • Set CORS headers appropriately

Example with JWT validation:

import jwt from 'jsonwebtoken';

export async function GET(request) {

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

if (!authHeader || !authHeader.startsWith('Bearer ')) {

return new Response(JSON.stringify({ error: 'Token required' }), { status: 401 });

}

const token = authHeader.substring(7);

try {

const decoded = jwt.verify(token, process.env.JWT_SECRET);

return new Response(JSON.stringify({ user: decoded }), { status: 200 });

} catch (error) {

return new Response(JSON.stringify({ error: 'Invalid token' }), { status: 403 });

}

}

Handle Errors Gracefully

Always wrap database calls, external API requests, and file operations in try-catch blocks. Return structured error responses:

export async function POST(request) {

try {

const data = await someAsyncOperation();

return new Response(JSON.stringify({ data }), { status: 200 });

} catch (error) {

console.error('API Error:', error);

if (error instanceof ValidationError) {

return new Response(JSON.stringify({ error: 'Validation failed', details: error.message }), {

status: 400,

});

}

if (error instanceof DatabaseError) {

return new Response(JSON.stringify({ error: 'Database error' }), {

status: 500,

});

}

return new Response(JSON.stringify({ error: 'Internal server error' }), {

status: 500,

});

}

}

Never expose stack traces or internal details to clients. Log errors server-side for debugging.

Optimize for Performance

API routes should respond quickly. Avoid blocking operations:

  • Use async/await for I/O operations
  • Cache responses with Redis or in-memory caches
  • Use revalidate for dynamic data
  • Minimize payload sizeonly return necessary fields
  • Enable compression via next.config.js
// next.config.js

module.exports = {

compress: true,

};

Tools and Resources

Recommended Libraries

  • Zod TypeScript-first schema validation
  • Prisma Type-safe ORM for PostgreSQL, MySQL, SQLite, and more
  • JWT JSON Web Tokens for authentication
  • Rate-Limiter-Flexible Rate limiting for Node.js
  • nodemailer Send emails from API routes
  • axios Make HTTP requests to external APIs
  • Lucia Lightweight authentication library for Next.js

Testing Tools

  • Postman Manually test API endpoints
  • Insomnia Open-source API client with collaboration features
  • Supertest Node.js library for testing HTTP servers
  • Vitest Fast unit testing framework for Next.js

Deployment Platforms

Next.js API routes deploy seamlessly on:

  • Vercel Official platform for Next.js; zero-config deployment
  • Netlify Supports API routes via Netlify Functions
  • Render Easy deployment with PostgreSQL integration
  • Fly.io Deploy globally with low latency

When deploying to Vercel, API routes are automatically converted into serverless functions. Be mindful of cold starts and timeout limits (10 seconds for Hobby, 60 seconds for Pro).

Documentation Tools

Document your API for frontend developers and external users:

  • Swagger/OpenAPI Generate interactive API documentation
  • Redoc Beautiful documentation UI
  • API Blueprint Markdown-based API specification

Use libraries like swagger-jsdoc to auto-generate OpenAPI specs from your route comments.

Monitoring and Logging

Production APIs need monitoring:

  • Sentry Error tracking and performance monitoring
  • LogRocket Session replay and error logging
  • Datadog Full-stack observability
  • Winston Logging library for structured logs

Log critical events like failed authentications, database errors, and rate limit hits.

Real Examples

Example 1: User Authentication System

Lets build a full authentication flow using API routes.

app/api/auth/register/route.js

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

import bcrypt from 'bcrypt';

const prisma = new PrismaClient();

export async function POST(request) {

const body = await request.json();

const { name, email, password } = body;

// Validate input

if (!name || !email || !password) {

return new Response(JSON.stringify({ error: 'All fields are required' }), {

status: 400,

});

}

// Check if user exists

const existingUser = await prisma.user.findUnique({ where: { email } });

if (existingUser) {

return new Response(JSON.stringify({ error: 'Email already in use' }), {

status: 409,

});

}

// Hash password

const hashedPassword = await bcrypt.hash(password, 10);

// Create user

const user = await prisma.user.create({

data: { name, email, password: hashedPassword },

});

return new Response(JSON.stringify({ id: user.id, email: user.email }), {

status: 201,

});

}

app/api/auth/login/route.js

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

import bcrypt from 'bcrypt';

import jwt from 'jsonwebtoken';

const prisma = new PrismaClient();

export async function POST(request) {

const body = await request.json();

const { email, password } = body;

if (!email || !password) {

return new Response(JSON.stringify({ error: 'Email and password required' }), {

status: 400,

});

}

const user = await prisma.user.findUnique({ where: { email } });

if (!user) {

return new Response(JSON.stringify({ error: 'Invalid credentials' }), {

status: 401,

});

}

const isValid = await bcrypt.compare(password, user.password);

if (!isValid) {

return new Response(JSON.stringify({ error: 'Invalid credentials' }), {

status: 401,

});

}

// Generate JWT

const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET, {

expiresIn: '7d',

});

return new Response(

JSON.stringify({

user: { id: user.id, email: user.email },

token,

}),

{

status: 200,

headers: {

'Set-Cookie': token=${token}; HttpOnly; Path=/; Max-Age=604800; Secure; SameSite=Strict,

},

}

);

}

Now your frontend can send login credentials and receive a secure token stored in an HTTP-only cookie.

Example 2: Webhook Handler for Stripe

Handle Stripe payment webhooks securely:

app/api/webhooks/stripe/route.js

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

import { Webhook } from 'stripe';

const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;

export async function POST(request) {

const buf = await request.arrayBuffer();

const signature = request.headers.get('Stripe-Signature');

const webhook = new Webhook(webhookSecret);

let event;

try {

event = webhook.constructEvent(buf, signature, webhookSecret);

} catch (err) {

return new Response(Webhook Error: ${err.message}, { status: 400 });

}

// Handle the event

switch (event.type) {

case 'payment_intent.succeeded':

const paymentIntent = event.data.object;

console.log(Payment succeeded for ${paymentIntent.amount});

// Update order status in database

break;

case 'customer.subscription.created':

const subscription = event.data.object;

console.log(Subscription created for customer ${subscription.customer});

break;

default:

console.log(Unhandled event type ${event.type});

}

return new Response(JSON.stringify({ received: true }), { status: 200 });

}

Configure this endpoint in your Stripe dashboard to receive real-time payment updates.

Example 3: Search Endpoint with External API

Fetch data from an external service like Google Places:

app/api/search/route.js

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

export async function GET(request) {

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

const query = searchParams.get('q');

if (!query) {

return new Response(JSON.stringify({ error: 'Query parameter required' }), {

status: 400,

});

}

const url = https://maps.googleapis.com/maps/api/place/textsearch/json?query=${encodeURIComponent(query)}&key=${process.env.GOOGLE_PLACES_API_KEY};

try {

const res = await fetch(url);

const data = await res.json();

if (!res.ok) {

return new Response(JSON.stringify({ error: data.error_message }), {

status: res.status,

});

}

return new Response(JSON.stringify(data.results), {

status: 200,

});

} catch (error) {

return new Response(JSON.stringify({ error: 'Failed to fetch data' }), {

status: 500,

});

}

}

Call it from your frontend with: /api/search?q=coffee+shop+new+york

FAQs

Can I use API routes in production?

Yes, Next.js API routes are production-ready. When deployed on Vercel or other platforms, they are compiled into serverless functions. For high-traffic applications, consider moving complex logic to a dedicated backend service to avoid cold starts and timeout limitations.

Do API routes support WebSockets?

No, Next.js API routes do not support WebSockets. For real-time communication, use a separate service like Socket.IO, Pusher, or Firebase Realtime Database.

Are API routes slower than a dedicated backend?

They can be, especially during cold starts on serverless platforms. For low-to-medium traffic apps, API routes are fast enough. For high-performance needs, consider using a Node.js backend with Express or Fastify.

Can I use TypeScript with API routes?

Absolutely. Name your files route.ts and use TypeScript interfaces for request and response types. Next.js provides excellent TypeScript support out of the box.

How do I test API routes locally?

Use tools like Postman, Insomnia, curl, or write unit tests with Vitest and Supertest. You can also test directly in the browser for GET requests.

Can I use API routes with a database other than PostgreSQL?

Yes. Prisma supports MySQL, SQLite, SQL Server, MongoDB, and more. You can also use Mongoose for MongoDB or direct drivers like pg or mysql2.

Whats the difference between API routes and server components?

Server components run on the server during rendering and return JSX. API routes are HTTP endpoints that return JSON or other data. Use server components for data fetching during page rendering; use API routes for dynamic, state-changing operations.

How do I handle CORS in Next.js API routes?

By default, Next.js API routes allow requests from any origin. To restrict them, set CORS headers manually:

export async function GET(request) {

return new Response(JSON.stringify({ data: 'hello' }), {

headers: {

'Access-Control-Allow-Origin': 'https://yourdomain.com',

'Access-Control-Allow-Methods': 'GET, POST',

'Access-Control-Allow-Headers': 'Content-Type, Authorization',

},

});

}

For development, you can also use the next.config.js file to configure global CORS.

Conclusion

Creating API routes in Next.js is one of the most powerful features that enables developers to build full-stack applications with minimal overhead. By leveraging Next.jss file-based routing system, you can expose secure, scalable, and maintainable endpoints without managing a separate backend server. Whether youre handling user authentication, processing payments, integrating third-party services, or building a headless CMS, API routes provide the flexibility and performance needed for modern web applications.

This guide walked you through the fundamentalsfrom setting up your first endpoint to implementing advanced patterns like middleware, rate limiting, and database integration. You learned how to structure routes logically, validate input, secure endpoints, and handle errors gracefully. Real-world examples demonstrated practical use cases, from Stripe webhooks to search APIs.

As Next.js continues to evolve, API routes will remain a cornerstone of its full-stack philosophy. By following the best practices outlined here, youre not just writing codeyoure building robust, production-ready systems that scale with your applications needs.

Start small, test thoroughly, and iterate. Your next project doesnt need a separate backendit just needs a well-structured app/api folder.