4. Route Handlers - A Detailed Guide
A document explaining app/api/**/route.ts - building HTTP APIs in Next.js (Stripe checkout, webhooks, NextAuth).
On this page
- Table of Contents
- 1. What Is a Route Handler?
- 2. Basic Syntax
- 3. Reading Requests & Returning Responses
- 4. Dynamic Routes & the NextAuth Catch-All
- 5. When to Use Route Handler vs Server Action
- 6. Nova Shop - the Three Main Endpoints
- 7. Stripe Webhook - Raw Body
- 8. Auth, Middleware, CORS
- 9. Next.js 15 - GET Is Dynamic by Default
- 10. FAQ & File Map
4. Route Handlers - A Detailed Guide
A document explaining app/api/**/route.ts - building HTTP APIs in Next.js (Stripe checkout, webhooks, NextAuth).
Read in order: Section 1 → 2 (syntax) → 3 (when to use) → 4–6 (Nova Stripe) → 7 (FAQ).
Docs: Route Handlers · Route Handlers getting started
Table of Contents
- What is a Route Handler?
- Basic syntax
- Reading requests & returning responses
- Dynamic routes & the NextAuth catch-all
- When to use Route Handler vs Server Action
- Nova Shop - the three main endpoints
- Stripe webhook - raw body
- Auth, middleware, CORS
- Next.js 15 - GET is dynamic by default
- FAQ & file map
1. What Is a Route Handler?
pages/api/hello.ts (old - Pages Router)
↓
app/api/hello/route.ts (App Router - Route Handler)
Page (page.tsx) | Route Handler (route.ts) | |
|---|---|---|
| Output | HTML / RSC UI | JSON, text, stream |
Navigate via <Link> | ✅ | ❌ |
Called via fetch | Possible | ✅ primary use |
Do not put page.tsx and route.ts in the same folder - Nova keeps app/api/* separate.
2. Basic Syntax
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
return NextResponse.json({ sessionId: "...", url: "..." });
}
| Exported function | HTTP method |
|---|---|
GET | GET |
POST | POST |
PUT, PATCH, DELETE | matching |
HEAD, OPTIONS | matching |
Missing method → 405 Method Not Allowed.
3. Reading Requests & Returning Responses
// JSON body (checkout)
const { productId, price } = await request.json();
// Form
const form = await request.formData();
// Raw text - REQUIRED for the Stripe webhook
const rawBody = await request.text();
// Headers
const sig = request.headers.get("stripe-signature");
// Query
const q = request.nextUrl.searchParams.get("query");
// Response
return NextResponse.json({ ok: true });
return new NextResponse("Forbidden", { status: 403 });
4. Dynamic Routes & the NextAuth Catch-All
app/api/auth/[...nextauth]/route.ts → /api/auth/*
NextAuth v5:
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
Next.js 15: params inside a handler is a Promise - const { team } = await params.
5. When to Use Route Handler vs Server Action
| Scenario | Choose |
|---|---|
| Add to cart, update qty | Server Action |
| Buy Now → Stripe URL | Route Handler POST /api/checkout |
| Stripe webhook | Route Handler POST /api/stripe/webhook |
| NextAuth OAuth callback | Route Handler [...nextauth] |
6. Nova Shop - the Three Main Endpoints
6.1 Buy Now - POST /api/checkout
Client BuyNowButton
→ fetch("/api/checkout", { body: { productId, price, quantity } })
→ createProductCheckoutSession()
→ { url } → window.location.href = url (Stripe Hosted)
File: app/api/checkout/route.ts → checkout-sessions.ts
6.2 Cart Checkout - POST /api/checkout/cart
cart-view handleCheckout
→ fetch("/api/checkout/cart")
→ getCartSummary() on the server
→ createCartCheckoutSession(items)
→ redirect to Stripe
File: app/api/checkout/cart/route.ts
6.3 Webhook - POST /api/stripe/webhook
Stripe servers
→ POST /api/stripe/webhook
→ handleStripeWebhook()
→ constructEvent(rawBody, signature)
→ revalidateAfterCartChange (refreshRoute: false)
File: app/api/stripe/webhook/route.ts
7. Stripe Webhook - Raw Body
Wrong - breaks the signature:
const body = await request.json(); // ❌
stripe.webhooks.constructEvent(body, ...);
Correct - Nova Shop:
const body = await request.text();
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
);
| Why | |
|---|---|
| Stripe signs the raw bytes | json() changes the format → signature verification fails |
After checkout.session.completed → revalidateProductsCatalog + revalidateAfterCartChange (not yet writing to the NestJS DB - TODO).
8. Auth, Middleware, CORS
Middleware usually excludes /api
// middleware.ts matcher
"/((?!api|_next/static|...).*)"
→ /api/checkout does not go through authorized - you must add auth in the handler if needed:
import { auth } from "@/auth";
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
Cart checkout: getCartSummary() uses the server-side cookie - implicit auth.
CORS
Nova: the business API lives in NestJS; Next Route Handlers are used for Stripe. If you expose a public API:
return NextResponse.json(data, {
headers: { "Access-Control-Allow-Origin": "https://allowed.com" },
});
9. Next.js 15 - GET Is Dynamic by Default
Before v15: a GET Route Handler could be statically cached.
v15: GET is dynamic by default.
To cache a GET route:
export const dynamic = "force-static";
export const revalidate = 60;
Nova's checkout only uses POST - unaffected.
Runtime: the Stripe SDK needs Node - you can add export const runtime = "nodejs".
10. FAQ & File Map
FAQ
Q: Does a Route Handler go through the ShopShell layout?
No - it only returns data, no UI.
Q: Should input be validated?
Always - return 400 if a required field is missing (productId, price).
Q: Can I log secrets inside a handler?
Never log STRIPE_SECRET_KEY or the webhook secret.
New Route Handler Checklist
- Correct method (POST for mutations).
- Validate the body.
- Webhook:
text()+ verify signature. - Missing env → a clear 500 message.
- Side effects →
revalidateTag/ call NestJS.
File Map
app/api/checkout/route.ts
app/api/checkout/cart/route.ts
app/api/stripe/webhook/route.ts
app/api/auth/[...nextauth]/route.ts
app/lib/checkout-sessions.ts → Stripe logic + webhook handler
Next: 5. Data Fetching · 3. Server Actions
