Module 15 — API Routes and Route Handlers in Next.js
API development is an essential part of building full-stack applications with Next.js.
In the App Router, Next.js provides Route Handlers for creating HTTP endpoints directly inside your application.
You can use Route Handlers to build APIs for:
- courses
- users
- authentication
- products
- orders
- payments
- search
- forms
- dashboards
- database operations
The core idea is simple:
1Client 2 ↓ 3HTTP Request 4 ↓ 5Next.js Route Handler 6 ↓ 7Validation 8 ↓ 9Business Logic 10 ↓ 11Database / External API 12 ↓ 13HTTP Response 14 ↓ 15Client
1. What Are Route Handlers?
Route Handlers allow you to create HTTP endpoints using files named:
1route.ts
inside the app directory.
For example:
1app/ 2└── api/ 3 └── courses/ 4 └── route.ts
This creates an API endpoint at:
1/api/courses
The route can handle HTTP methods such as:
1GET 2POST 3PUT 4PATCH 5DELETE
2. Your First Route Handler
Create:
1app/api/courses/route.ts
Then:
1import { NextResponse } from "next/server"; 2 3export async function GET() { 4 const courses = [ 5 { 6 id: 1, 7 title: "Next.js Fundamentals", 8 }, 9 { 10 id: 2, 11 title: "Advanced React", 12 }, 13 ]; 14 15 return NextResponse.json(courses); 16}
Now a request to:
1GET /api/courses
returns:
1[ 2 { 3 "id": 1, 4 "title": "Next.js Fundamentals" 5 }, 6 { 7 "id": 2, 8 "title": "Advanced React" 9 } 10]
The architecture is:
1Browser 2 │ 3 │ GET /api/courses 4 ▼ 5app/api/courses/route.ts 6 │ 7 ▼ 8GET() 9 │ 10 ▼ 11JSON Response
3. GET Requests
GET is normally used to retrieve data.
Example:
1import { NextResponse } from "next/server"; 2 3export async function GET() { 4 const courses = await getCourses(); 5 6 return NextResponse.json(courses); 7}
The endpoint:
1GET /api/courses
might return:
1[ 2 { 3 "id": "1", 4 "title": "Next.js" 5 }, 6 { 7 "id": "2", 8 "title": "React" 9 } 10]
A GET request should generally be used for reading resources rather than modifying them.
4. POST Requests
POST is commonly used to create new resources or submit data.
Example:
1import { NextResponse } from "next/server"; 2 3export async function POST(request: Request) { 4 const body = await request.json(); 5 6 const course = await createCourse(body); 7 8 return NextResponse.json(course, { 9 status: 201, 10 }); 11}
A client might send:
1{ 2 "title": "Advanced Next.js", 3 "description": "Learn advanced Next.js concepts" 4}
The flow is:
1Client 2 │ 3 │ POST /api/courses 4 │ 5 │ JSON body 6 ▼ 7Route Handler 8 │ 9 ▼ 10Validate input 11 │ 12 ▼ 13Create course 14 │ 15 ▼ 16201 Created
5. Reading the Request Body
Route Handlers receive a standard Web Request object.
For JSON data:
1export async function POST(request: Request) { 2 const body = await request.json(); 3 4 console.log(body); 5 6 return Response.json({ 7 success: true, 8 }); 9}
For example, if the client sends:
1{ 2 "name": "Ankit", 3 "email": "ankit@example.com" 4}
then:
1const body = await request.json();
produces an object containing those fields.
You can then access:
1const name = body.name; 2const email = body.email;
6. Validate Request Bodies
Never blindly trust incoming request data.
This is unsafe:
1export async function POST(request: Request) { 2 const body = await request.json(); 3 4 await createUser(body); 5 6 return Response.json({ 7 success: true, 8 }); 9}
The client could send:
1{ 2 "name": 123, 3 "email": null 4}
Instead, validate the input before using it.
A common approach is to use a schema validation library such as Zod.
1import { z } from "zod"; 2 3const courseSchema = z.object({ 4 title: z.string().min(3), 5 description: z.string().min(10), 6});
Then:
1export async function POST(request: Request) { 2 const body = await request.json(); 3 4 const result = courseSchema.safeParse(body); 5 6 if (!result.success) { 7 return Response.json( 8 { 9 error: "Invalid request data", 10 }, 11 { 12 status: 400, 13 } 14 ); 15 } 16 17 const course = await createCourse(result.data); 18 19 return Response.json(course, { 20 status: 201, 21 }); 22}
The flow becomes:
1Request 2 ↓ 3Parse JSON 4 ↓ 5Validate 6 │ 7 ├── Invalid → 400 8 │ 9 └── Valid 10 ↓ 11 Business logic 12 ↓ 13 Database
7. PUT Requests
PUT is commonly used to replace an existing resource.
For example:
1PUT /api/courses/123
A Route Handler can be structured like:
1export async function PUT( 2 request: Request, 3 context: { 4 params: Promise<{ id: string }>; 5 } 6) { 7 const { id } = await context.params; 8 9 const body = await request.json(); 10 11 const updatedCourse = await replaceCourse(id, body); 12 13 return Response.json(updatedCourse); 14}
Conceptually:
1Existing resource 2 ↓ 3PUT 4 ↓ 5Replace representation 6 ↓ 7Updated resource
8. PATCH Requests
PATCH is generally used for partial updates.
Suppose a course contains:
1{ 2 "title": "Next.js", 3 "description": "Learn Next.js", 4 "published": false 5}
You might only want to update:
1{ 2 "published": true 3}
A PATCH request is appropriate:
1PATCH /api/courses/123
Example:
1export async function PATCH( 2 request: Request, 3 context: { 4 params: Promise<{ id: string }>; 5 } 6) { 7 const { id } = await context.params; 8 9 const body = await request.json(); 10 11 const course = await updateCourse(id, body); 12 13 return Response.json(course); 14}
The distinction is:
1PUT 2 ↓ 3Replace resource 4 5PATCH 6 ↓ 7Modify part of resource
9. DELETE Requests
DELETE is used to remove a resource.
Example:
1export async function DELETE( 2 request: Request, 3 context: { 4 params: Promise<{ id: string }>; 5 } 6) { 7 const { id } = await context.params; 8 9 await deleteCourse(id); 10 11 return new Response(null, { 12 status: 204, 13 }); 14}
The request:
1DELETE /api/courses/123
might remove course 123.
The flow:
1DELETE request 2 ↓ 3Authenticate 4 ↓ 5Authorize 6 ↓ 7Find resource 8 ↓ 9Delete resource 10 ↓ 11204 No Content
10. Route Parameters
Dynamic folders can create dynamic API routes.
For example:
1app/ 2└── api/ 3 └── courses/ 4 └── [id]/ 5 └── route.ts
This represents:
1/api/courses/:id
For example:
1/api/courses/123 2/api/courses/456 3/api/courses/789
The route handler can read the parameter:
1export async function GET( 2 request: Request, 3 context: { 4 params: Promise<{ id: string }>; 5 } 6) { 7 const { id } = await context.params; 8 9 const course = await getCourse(id); 10 11 return Response.json(course); 12}
The important concept is:
1/api/courses/[id] 2 ↑ 3 dynamic 4 parameter
11. Query Parameters
Query parameters appear after ? in a URL.
Example:
1/api/courses?category=nextjs&limit=10
You can read them with request.url.
A convenient approach is:
1export async function GET(request: Request) { 2 const { searchParams } = new URL(request.url); 3 4 const category = searchParams.get("category"); 5 const limit = searchParams.get("limit"); 6 7 const courses = await getCourses({ 8 category, 9 limit, 10 }); 11 12 return Response.json(courses); 13}
For:
1/api/courses?category=nextjs&limit=10
you get:
1category = "nextjs" 2limit = "10"
Remember that query parameters arrive as strings.
If you need a number:
1const limit = Number(searchParams.get("limit") ?? "10");
You should also validate the resulting value.
12. Route Parameters vs Query Parameters
These two concepts are easy to confuse.
Route parameter
1/api/courses/123
Here:
1123
identifies a specific resource.
Query parameter
1/api/courses?category=nextjs
Here:
1category=nextjs
modifies or filters the request.
Think:
1Route parameter 2 ↓ 3Which resource? 4 5Query parameter 6 ↓ 7How should I query/filter it?
13. Headers
HTTP headers contain additional information about a request.
Examples:
1Authorization 2Content-Type 3Accept 4User-Agent
You can read headers:
1export async function GET(request: Request) { 2 const authorization = request.headers.get( 3 "authorization" 4 ); 5 6 return Response.json({ 7 authenticated: 8 authorization !== null, 9 }); 10}
You can also inspect:
1const contentType = request.headers.get( 2 "content-type" 3);
Headers are commonly used for:
1Authentication 2Content negotiation 3Request metadata 4API versioning 5Client information
14. Authentication Headers
A common API authentication pattern is:
1Authorization: Bearer <token>
A Route Handler can read it:
1export async function GET(request: Request) { 2 const authorization = request.headers.get( 3 "authorization" 4 ); 5 6 if (!authorization) { 7 return Response.json( 8 { 9 error: "Unauthorized", 10 }, 11 { 12 status: 401, 13 } 14 ); 15 } 16 17 // Validate token here. 18 19 return Response.json({ 20 message: "Authenticated", 21 }); 22}
In production, do not simply check whether a header exists.
You must actually validate the credential.
15. Cookies
Cookies can be accessed in Route Handlers.
For example:
1import { cookies } from "next/headers"; 2 3export async function GET() { 4 const cookieStore = await cookies(); 5 6 const session = cookieStore.get( 7 "session" 8 ); 9 10 return Response.json({ 11 authenticated: Boolean(session), 12 }); 13}
Cookies are commonly used for:
1Session IDs 2Authentication 3Preferences 4Security tokens 5Feature settings
For authentication systems, secure cookie configuration is especially important.
16. Setting Cookies
Route Handlers can also set cookies.
1import { cookies } from "next/headers"; 2 3export async function POST() { 4 const cookieStore = await cookies(); 5 6 cookieStore.set("session", "example-session-id", { 7 httpOnly: true, 8 secure: true, 9 sameSite: "lax", 10 path: "/", 11 }); 12 13 return Response.json({ 14 success: true, 15 }); 16}
Important security-related options include:
1httpOnly 2secure 3sameSite 4path 5expires 6maxAge
For authentication cookies, httpOnly is particularly useful because JavaScript running in the browser cannot directly read the cookie.
17. Authentication vs Authorization
These concepts are different.
Authentication
Answers:
Who are you?
1User 2 ↓ 3Login 4 ↓ 5Identity verified
Authorization
Answers:
Are you allowed to perform this operation?
1Authenticated user 2 ↓ 3Check permissions 4 ↓ 5Allowed / Denied
A Route Handler should often perform both.
1Request 2 ↓ 3Authentication 4 ↓ 5Authorization 6 ↓ 7Validation 8 ↓ 9Business logic
18. Protected Route Example
Suppose only authenticated users can create courses.
1import { NextResponse } from "next/server"; 2 3export async function POST(request: Request) { 4 const user = await getCurrentUser(); 5 6 if (!user) { 7 return NextResponse.json( 8 { 9 error: "Unauthorized", 10 }, 11 { 12 status: 401, 13 } 14 ); 15 } 16 17 const body = await request.json(); 18 19 const course = await createCourse({ 20 ...body, 21 authorId: user.id, 22 }); 23 24 return NextResponse.json(course, { 25 status: 201, 26 }); 27}
The architecture is:
1POST /api/courses 2 ↓ 3Get current user 4 ↓ 5Authenticated? 6 ┌───┴───┐ 7 NO YES 8 │ │ 9 401 Validate 10 │ 11 ▼ 12 Create course 13 │ 14 ▼ 15 201
19. Authorization Example
Suppose only administrators can delete courses.
1export async function DELETE( 2 request: Request, 3 context: { 4 params: Promise<{ id: string }>; 5 } 6) { 7 const user = await getCurrentUser(); 8 9 if (!user) { 10 return Response.json( 11 { 12 error: "Unauthorized", 13 }, 14 { 15 status: 401, 16 } 17 ); 18 } 19 20 if (user.role !== "admin") { 21 return Response.json( 22 { 23 error: "Forbidden", 24 }, 25 { 26 status: 403, 27 } 28 ); 29 } 30 31 const { id } = await context.params; 32 33 await deleteCourse(id); 34 35 return new Response(null, { 36 status: 204, 37 }); 38}
Notice the difference:
1401 Unauthorized 2↓ 3Not authenticated 4 5403 Forbidden 6↓ 7Authenticated but not allowed
20. HTTP Status Codes
HTTP status codes communicate the result of a request.
2xx — Success
1200 OK 2201 Created 3202 Accepted 4204 No Content
Common usage:
1GET → 200 2POST → 201 3DELETE → 204
4xx — Client Error
1400 Bad Request 2401 Unauthorized 3403 Forbidden 4404 Not Found 5409 Conflict 6422 Unprocessable Content 7429 Too Many Requests
Examples:
1400 → Invalid request 2401 → Not authenticated 3403 → Not allowed 4404 → Resource doesn't exist 5409 → Resource conflict 6429 → Rate limit exceeded
5xx — Server Error
1500 Internal Server Error 2502 Bad Gateway 3503 Service Unavailable 4504 Gateway Timeout
These generally indicate a server-side or upstream failure.
21. Returning Error Responses
A clean error response might look like:
1return Response.json( 2 { 3 error: "Course not found", 4 }, 5 { 6 status: 404, 7 } 8);
For validation:
1return Response.json( 2 { 3 error: "Invalid request", 4 fields: { 5 title: "Title is required", 6 }, 7 }, 8 { 9 status: 400, 10 } 11);
The client can then inspect the status.
22. A Complete CRUD API
Suppose we want a course API.
Our structure could be:
1app/ 2└── api/ 3 └── courses/ 4 ├── route.ts 5 │ 6 └── [id]/ 7 └── route.ts
Collection endpoint
1GET /api/courses 2POST /api/courses
Individual resource
1GET /api/courses/123 2PUT /api/courses/123 3PATCH /api/courses/123 4DELETE /api/courses/123
This gives us a standard REST-style structure.
23. Complete GET and POST Route
1import { NextResponse } from "next/server"; 2 3export async function GET() { 4 const courses = await getCourses(); 5 6 return NextResponse.json(courses); 7} 8 9export async function POST(request: Request) { 10 const body = await request.json(); 11 12 if (!body.title) { 13 return NextResponse.json( 14 { 15 error: "Title is required", 16 }, 17 { 18 status: 400, 19 } 20 ); 21 } 22 23 const course = await createCourse({ 24 title: body.title, 25 description: body.description, 26 }); 27 28 return NextResponse.json(course, { 29 status: 201, 30 }); 31}
One route.ts file can export multiple HTTP method handlers.
24. Complete Individual Course Route
1import { NextResponse } from "next/server"; 2 3type Context = { 4 params: Promise<{ 5 id: string; 6 }>; 7}; 8 9export async function GET( 10 request: Request, 11 context: Context 12) { 13 const { id } = await context.params; 14 15 const course = await getCourse(id); 16 17 if (!course) { 18 return NextResponse.json( 19 { 20 error: "Course not found", 21 }, 22 { 23 status: 404, 24 } 25 ); 26 } 27 28 return NextResponse.json(course); 29} 30 31export async function PATCH( 32 request: Request, 33 context: Context 34) { 35 const { id } = await context.params; 36 37 const body = await request.json(); 38 39 const course = await updateCourse(id, body); 40 41 return NextResponse.json(course); 42} 43 44export async function DELETE( 45 request: Request, 46 context: Context 47) { 48 const { id } = await context.params; 49 50 await deleteCourse(id); 51 52 return new Response(null, { 53 status: 204, 54 }); 55}
This provides:
1GET → Read 2PATCH → Update 3DELETE → Remove
25. NextResponse vs Response
You can use the standard Web API:
1return Response.json({ 2 success: true, 3});
You can also use:
1import { NextResponse } from "next/server"; 2 3return NextResponse.json({ 4 success: true, 5});
Both are useful.
The standard Response API is based on the Web platform, while NextResponse provides Next.js-specific conveniences.
For simple JSON APIs, either approach can be appropriate.
26. POST Request from a Client Component
A Client Component can call your Route Handler:
1"use client"; 2 3export default function CreateCourse() { 4 async function createCourse() { 5 const response = await fetch("/api/courses", { 6 method: "POST", 7 headers: { 8 "Content-Type": "application/json", 9 }, 10 body: JSON.stringify({ 11 title: "Next.js Advanced", 12 description: "Advanced Next.js course", 13 }), 14 }); 15 16 if (!response.ok) { 17 throw new Error("Failed to create course"); 18 } 19 20 const course = await response.json(); 21 22 console.log(course); 23 } 24 25 return ( 26 <button onClick={createCourse}> 27 Create Course 28 </button> 29 ); 30}
The complete request flow:
1Button click 2 ↓ 3fetch() 4 ↓ 5POST /api/courses 6 ↓ 7Route Handler 8 ↓ 9Validate 10 ↓ 11Database 12 ↓ 13201 Created 14 ↓ 15JSON response 16 ↓ 17Client UI
27. Form Data
Route Handlers can also receive form submissions.
For example:
1export async function POST(request: Request) { 2 const formData = await request.formData(); 3 4 const title = formData.get("title"); 5 6 return Response.json({ 7 title, 8 }); 9}
The request can contain:
1title=Next.js
Form data is particularly useful for:
1File uploads 2HTML forms 3Multipart requests
28. File Uploads
For multipart form data:
1export async function POST(request: Request) { 2 const formData = await request.formData(); 3 4 const file = formData.get("file"); 5 6 if (!(file instanceof File)) { 7 return Response.json( 8 { 9 error: "File is required", 10 }, 11 { 12 status: 400, 13 } 14 ); 15 } 16 17 console.log(file.name); 18 console.log(file.size); 19 console.log(file.type); 20 21 return Response.json({ 22 success: true, 23 }); 24}
Production applications should additionally validate:
1File size 2MIME type 3Extension 4Content 5Authentication 6Authorization 7Storage destination
Never trust file metadata supplied by the client.
29. Request Validation Pipeline
A production API should generally follow a predictable sequence:
1Incoming Request 2 ↓ 3Parse request 4 ↓ 5Authenticate 6 ↓ 7Authorize 8 ↓ 9Validate input 10 ↓ 11Business logic 12 ↓ 13Database / External API 14 ↓ 15Return response
For example:
1export async function POST(request: Request) { 2 // 1. Authentication 3 const user = await getCurrentUser(); 4 5 if (!user) { 6 return Response.json( 7 { error: "Unauthorized" }, 8 { status: 401 } 9 ); 10 } 11 12 // 2. Parse 13 const body = await request.json(); 14 15 // 3. Validate 16 if (!body.title) { 17 return Response.json( 18 { error: "Title is required" }, 19 { status: 400 } 20 ); 21 } 22 23 // 4. Business logic 24 const course = await createCourse({ 25 title: body.title, 26 authorId: user.id, 27 }); 28 29 // 5. Response 30 return Response.json(course, { 31 status: 201, 32 }); 33}
This pattern is reusable across many APIs.
30. Preventing Information Leakage
Avoid returning sensitive information.
Bad:
1return Response.json({ 2 user, 3 password: user.password, 4 internalToken: user.internalToken, 5});
Instead, return only what the client needs:
1return Response.json({ 2 id: user.id, 3 name: user.name, 4 email: user.email, 5});
API responses should follow the principle:
Return the minimum information necessary.
31. Authentication Architecture
A common architecture is:
1Browser 2 │ 3 │ Cookie / Session 4 ▼ 5Route Handler 6 │ 7 ▼ 8Authentication system 9 │ 10 ▼ 11Current user 12 │ 13 ▼ 14Authorization 15 │ 16 ▼ 17Business operation
For example:
1export async function GET() { 2 const user = await getCurrentUser(); 3 4 if (!user) { 5 return Response.json( 6 { 7 error: "Unauthorized", 8 }, 9 { 10 status: 401, 11 } 12 ); 13 } 14 15 const courses = await getCoursesForUser(user.id); 16 17 return Response.json(courses); 18}
The API determines the user on the server rather than trusting a user ID supplied by the browser.
32. Never Trust Client Input
A client could send:
1{ 2 "userId": "admin-user-id" 3}
That does not mean the request should operate as that user.
Instead:
1Request 2 ↓ 3Authenticated session 4 ↓ 5Server identifies user 6 ↓ 7Server checks permissions 8 ↓ 9Operation
The server should be the authority for:
1Identity 2Permissions 3Ownership 4Roles 5Security
33. API Route Organization
A scalable API might look like:
1app/ 2└── api/ 3 ├── auth/ 4 │ └── login/ 5 │ └── route.ts 6 │ 7 ├── courses/ 8 │ ├── route.ts 9 │ └── [id]/ 10 │ └── route.ts 11 │ 12 ├── users/ 13 │ ├── route.ts 14 │ └── [id]/ 15 │ └── route.ts 16 │ 17 └── orders/ 18 ├── route.ts 19 └── [id]/ 20 └── route.ts
This structure maps naturally to URLs.
34. REST API Design
A clean REST-style API might use:
1GET /api/courses 2POST /api/courses 3 4GET /api/courses/123 5PUT /api/courses/123 6PATCH /api/courses/123 7DELETE /api/courses/123
For users:
1GET /api/users 2POST /api/users 3 4GET /api/users/123 5PATCH /api/users/123 6DELETE /api/users/123
The HTTP method communicates the operation.
1GET 2 ↓ 3Read 4 5POST 6 ↓ 7Create 8 9PUT 10 ↓ 11Replace 12 13PATCH 14 ↓ 15Partially update 16 17DELETE 18 ↓ 19Delete
35. Common Mistakes
Mistake 1 — Not validating input
Never assume client data is valid.
Mistake 2 — Returning 200 for every error
Use meaningful status codes.
1400 2401 3403 4404 5409 6422 7500
where appropriate.
Mistake 3 — Trusting client-supplied user IDs
Use authenticated server-side identity.
Mistake 4 — Exposing secrets
Never send API keys, passwords, or private tokens to clients.
Mistake 5 — Mixing authentication and authorization
A logged-in user does not automatically have permission to perform every action.
Mistake 6 — Ignoring failed API requests
Always consider:
1if (!response.ok) { 2 // Handle failure 3}
Mistake 7 — Returning too much data
Only expose the fields the client needs.
36. Complete Production API Flow
A mature Route Handler often follows this architecture:
1 HTTP Request 2 │ 3 ▼ 4 Route Handler 5 │ 6 ▼ 7 Parse Request 8 │ 9 ▼ 10 Authentication 11 │ 12 ┌─────┴─────┐ 13 │ │ 14 NO YES 15 │ │ 16 401 ▼ 17 Authorization 18 │ 19 ┌─────┴─────┐ 20 │ │ 21 NO YES 22 │ │ 23 403 ▼ 24 Validate Input 25 │ 26 ┌─────┴─────┐ 27 │ │ 28 Invalid Valid 29 │ │ 30 400 ▼ 31 Business Logic 32 │ 33 ▼ 34 Database / API 35 │ 36 ▼ 37 HTTP Response
This pipeline is extremely useful when designing production APIs.
37. Complete Example — Course API
app/api/courses/route.ts
1import { NextResponse } from "next/server"; 2import { z } from "zod"; 3 4const courseSchema = z.object({ 5 title: z.string().min(3), 6 description: z.string().min(10), 7}); 8 9export async function GET() { 10 const courses = await getCourses(); 11 12 return NextResponse.json(courses); 13} 14 15export async function POST(request: Request) { 16 const user = await getCurrentUser(); 17 18 if (!user) { 19 return NextResponse.json( 20 { 21 error: "Unauthorized", 22 }, 23 { 24 status: 401, 25 } 26 ); 27 } 28 29 const body = await request.json(); 30 31 const result = courseSchema.safeParse(body); 32 33 if (!result.success) { 34 return NextResponse.json( 35 { 36 error: "Invalid course data", 37 }, 38 { 39 status: 400, 40 } 41 ); 42 } 43 44 const course = await createCourse({ 45 title: result.data.title, 46 description: result.data.description, 47 authorId: user.id, 48 }); 49 50 return NextResponse.json(course, { 51 status: 201, 52 }); 53}
This single route demonstrates:
1GET 2POST 3Authentication 4Validation 5Database operation 6JSON response 7HTTP status codes
38. Key Takeaways
Route Handlers allow Next.js to act as a full-stack application framework.
Remember the core structure:
1app/api/courses/route.ts
creates:
1/api/courses
and can export:
1GET 2POST 3PUT 4PATCH 5DELETE
The fundamental mapping is:
1GET 2 ↓ 3Read 4 5POST 6 ↓ 7Create 8 9PUT 10 ↓ 11Replace 12 13PATCH 14 ↓ 15Update part 16 17DELETE 18 ↓ 19Delete
A production Route Handler should generally think in this order:
1Request 2 ↓ 3Parse 4 ↓ 5Authenticate 6 ↓ 7Authorize 8 ↓ 9Validate 10 ↓ 11Business Logic 12 ↓ 13Database / API 14 ↓ 15Response
And always remember:
1Client input is untrusted. 2Server validation is required. 3Authentication identifies the user. 4Authorization checks permissions. 5HTTP status codes communicate the result. 6Secrets remain on the server.
Once you understand Route Handlers, you can build complete APIs directly inside Next.js and connect your frontend, database, authentication system, and external services through a clean HTTP architecture.