Adding API Routes
Learn how to create new API endpoints in your SaaS application using Next.js App Router
Advanced Developer Content
This guide requires technical knowledge:
- Basic understanding of REST APIs (GET, POST, PUT, DELETE)
- Familiarity with Next.js App Router structure
- Knowledge of TypeScript and async/await
This guide shows you how to add new API routes using Next.js 15 App Router. Follow the Todo API pattern for consistency.
API routes are located in src/app/api/ with each route defined in a route.ts file. See the todos folder for a working example.
Creating a Simple API Route
Create the Route File
Create a new folder under src/app/api/ and add a route.ts file:
1# Create a new API route for projects2mkdir -p src/app/api/projects3touch src/app/api/projects/route.tsDefine HTTP Methods
Each exported function represents an HTTP method. Here's a basic GET endpoint:
1import { NextRequest, NextResponse } from "next/server";2 3export async function GET(request: NextRequest) {4 try {5 // Your logic here6 const projects = [7 { id: "1", name: "Project Alpha" },8 { id: "2", name: "Project Beta" }9 ];10 11 return NextResponse.json({ projects });12 } catch (error) {13 console.error("Error fetching projects:", error);14 return NextResponse.json(15 { error: "Failed to fetch projects" },16 { status: 500 }17 );18 }19}Test Your Endpoint
Start your dev server and test the endpoint:
1npm run dev2 3# Test with curl4curl http://localhost:3000/api/projectsYou should receive a JSON response with the project list.
Adding Authentication
Most API routes should be protected. Here's how to add authentication using Auth.js:
1import { NextRequest, NextResponse } from "next/server";2import { auth } from "@/lib/auth";3 4export async function GET(request: NextRequest) {5 // Check authentication6 const session = await auth();7 if (!session?.user) {8 return NextResponse.json(9 { error: "Unauthorized" },10 { status: 401 }11 );12 }13 14 try {15 // Your authenticated logic here16 const projects = await fetchUserProjects(session.user.id);17 return NextResponse.json({ projects });18 } catch (error) {19 console.error("Error fetching projects:", error);20 return NextResponse.json(21 { error: "Failed to fetch projects" },22 { status: 500 }23 );24 }25}Always Validate Sessions
await auth() before processing requests.Working with Database
Use the Prisma client to interact with the database:
1import { NextRequest, NextResponse } from "next/server";2import { auth } from "@/lib/auth";3import { prisma } from "@/lib/db";4 5export async function GET(request: NextRequest) {6 const session = await auth();7 if (!session?.user) {8 return NextResponse.json(9 { error: "Unauthorized" },10 { status: 401 }11 );12 }13 14 try {15 // Fetch projects from database16 const projects = await prisma.project.findMany({17 where: {18 userId: session.user.id19 },20 orderBy: {21 createdAt: "desc"22 }23 });24 25 return NextResponse.json({ projects });26 } catch (error) {27 console.error("Error fetching projects:", error);28 return NextResponse.json(29 { error: "Failed to fetch projects" },30 { status: 500 }31 );32 }33}Handling POST Requests
To create resources, add a POST handler with request body validation:
1export async function POST(request: NextRequest) {2 const session = await auth();3 if (!session?.user) {4 return NextResponse.json(5 { error: "Unauthorized" },6 { status: 401 }7 );8 }9 10 try {11 // Parse request body12 const body = await request.json();13 const { name, description } = body;14 15 // Validate input16 if (!name || typeof name !== "string") {17 return NextResponse.json(18 { error: "Project name is required" },19 { status: 400 }20 );21 }22 23 // Create project in database24 const project = await prisma.project.create({25 data: {26 name,27 description: description || null,28 userId: session.user.id29 }30 });31 32 return NextResponse.json(33 { project },34 { status: 201 } // Created35 );36 } catch (error) {37 console.error("Error creating project:", error);38 return NextResponse.json(39 { error: "Failed to create project" },40 { status: 500 }41 );42 }43}Validation
zod to define schemas and validate request bodies safely.Best Practices
- Always check authentication - Use
await auth()at the start of every protected route - Validate user input - Never trust client data, validate all request bodies and parameters
- Filter queries by userId - Ensure users can only access their own data
- Use try-catch for errors - Wrap all database operations in try-catch blocks
- Return appropriate HTTP status codes - 200/201 for success, 400 for validation errors, 401 for auth failures, 500 for server errors
- Check resource ownership - Always verify
userId === session.user.idbefore update/delete operations
For advanced patterns (admin routes, rate limiting, WebSockets), see the Next.js Route Handlers Documentation.
Example: Complete CRUD API
Here's a complete example combining all concepts:
List & Create (src/app/api/projects/route.ts)
1import { NextRequest, NextResponse } from "next/server";2import { auth } from "@/lib/auth";3import { prisma } from "@/lib/db";4 5// GET /api/projects - List all projects for current user6export async function GET(request: NextRequest) {7 const session = await auth();8 if (!session?.user) {9 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });10 }11 12 try {13 const projects = await prisma.project.findMany({14 where: { userId: session.user.id },15 orderBy: { createdAt: "desc" }16 });17 return NextResponse.json({ projects });18 } catch (error) {19 console.error("Error fetching projects:", error);20 return NextResponse.json(21 { error: "Failed to fetch projects" },22 { status: 500 }23 );24 }25}26 27// POST /api/projects - Create a new project28export async function POST(request: NextRequest) {29 const session = await auth();30 if (!session?.user) {31 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });32 }33 34 try {35 const body = await request.json();36 const { name, description } = body;37 38 if (!name || typeof name !== "string") {39 return NextResponse.json(40 { error: "Project name is required" },41 { status: 400 }42 );43 }44 45 const project = await prisma.project.create({46 data: {47 name,48 description: description || null,49 userId: session.user.id50 }51 });52 53 return NextResponse.json({ project }, { status: 201 });54 } catch (error) {55 console.error("Error creating project:", error);56 return NextResponse.json(57 { error: "Failed to create project" },58 { status: 500 }59 );60 }61}Get, Update & Delete (src/app/api/projects/[id]/route.ts)
1import { NextRequest, NextResponse } from "next/server";2import { auth } from "@/lib/auth";3import { prisma } from "@/lib/db";4 5type Params = { params: { id: string } };6 7// GET /api/projects/[id] - Get single project8export async function GET(request: NextRequest, { params }: Params) {9 const session = await auth();10 if (!session?.user) {11 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });12 }13 14 try {15 const project = await prisma.project.findUnique({16 where: { id: params.id, userId: session.user.id }17 });18 19 if (!project) {20 return NextResponse.json(21 { error: "Project not found" },22 { status: 404 }23 );24 }25 26 return NextResponse.json({ project });27 } catch (error) {28 console.error("Error fetching project:", error);29 return NextResponse.json(30 { error: "Failed to fetch project" },31 { status: 500 }32 );33 }34}35 36// PUT /api/projects/[id] - Update project37export async function PUT(request: NextRequest, { params }: Params) {38 const session = await auth();39 if (!session?.user) {40 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });41 }42 43 try {44 const body = await request.json();45 const { name, description } = body;46 47 const project = await prisma.project.update({48 where: { id: params.id, userId: session.user.id },49 data: { name, description }50 });51 52 return NextResponse.json({ project });53 } catch (error) {54 console.error("Error updating project:", error);55 return NextResponse.json(56 { error: "Failed to update project" },57 { status: 500 }58 );59 }60}61 62// DELETE /api/projects/[id] - Delete project63export async function DELETE(request: NextRequest, { params }: Params) {64 const session = await auth();65 if (!session?.user) {66 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });67 }68 69 try {70 await prisma.project.delete({71 where: { id: params.id, userId: session.user.id }72 });73 74 return NextResponse.json({ message: "Project deleted" });75 } catch (error) {76 console.error("Error deleting project:", error);77 return NextResponse.json(78 { error: "Failed to delete project" },79 { status: 500 }80 );81 }82}Troubleshooting
Error: "Cannot read properties of undefined"
- Check that
session.userexists before accessing properties - Ensure Auth.js is properly configured in
src/lib/auth.ts - Verify the user is logged in when testing
Error: "Prisma Client validation error"
- Make sure your database model exists: check
prisma/schema.prisma - Run
npx prisma generateto update the Prisma client - Verify field names match your schema exactly
Routes Not Working in Production
- Ensure
route.tsfiles are named correctly (notroute.tsx) - Check that exported functions match HTTP method names exactly (GET, POST, PUT, DELETE)
- Rebuild your application:
npm run build
Related Guides
Next Steps
Now that you know how to create API routes, you might want to:
- Learn about Adding Database Models to store your data
- Explore Creating New Features for the complete development workflow
- Review User Management for user-scoped data operations