Next.js API Routes for Bangkok Business Websites: Best Practices for 2026
28 April 2026 · by Yunmin Shin
What Are Next.js API Routes in 2026?
Next.js gives you two mechanisms for server-side logic: Route Handlers (route.ts files in the App Router) and Server Actions ("use server" functions). Choosing correctly between them is the first best practice, and it matters more than it sounds once real integrations show up.
Use a Route Handler when something outside your own frontend needs to call it — a LINE webhook delivering a message event, a PromptPay payment gateway posting a payment confirmation, or a booking widget embedded on a partner site. Use a Server Action for mutations that originate from your own Next.js frontend, like a clinic staff member updating a booking status from the admin dashboard. Server Actions are simpler and integrate directly with React's form APIs; webhooks and third-party integrations have no choice but to be Route Handlers.
How Should You Validate Incoming Data?
Never trust incoming request data, especially from a public booking form. Validate everything with Zod before it touches your database:
import { z } from "zod";
const bookingSchema = z.object({
treatmentId: z.string().uuid(),
slotStart: z.string().datetime(),
patientName: z.string().min(1).max(100),
phone: z.string().regex(/^0\d{8,9}$/), // Thai mobile format
channel: z.enum(["line", "phone", "walk_in"]),
});
export async function POST(request: Request) {
const body = await request.json();
const result = bookingSchema.safeParse(body);
if (!result.success) {
return Response.json({ error: result.error.flatten() }, { status: 400 });
}
// result.data is fully typed and validated
}
This pattern rejects malformed booking requests — a common source of them is a mobile browser autofill mangling a phone number — before they reach the database, and gives the frontend a clear, structured error to display.
How Do You Handle Errors Consistently?
Define custom error classes (SlotUnavailableError, TreatmentNotFoundError, UnauthorizedError) and map them to HTTP status codes in a centralized handler. Return errors in a consistent shape across every route:
{ "error": "That slot was just booked", "code": "SLOT_TAKEN" }
A booking system is especially prone to race conditions — two customers hitting "confirm" on the same slot within the same second is a real scenario for a popular clinic on a Friday evening. A consistent, machine-readable error code lets the frontend show "please pick another time" instead of a generic failure message.
What About Rate Limiting?
Any public route needs rate limiting, and the routes that need it most are exactly the ones connected to something that costs money per call: an AI chatbot endpoint burning Claude API credits, or a booking endpoint that could be hammered by a script. Use Upstash Redis with @upstash/ratelimit for serverless-compatible limiting:
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "1 m"),
});
Apply limits per IP for public endpoints (a booking form, a public search) and per user ID for authenticated ones (a clinic staff dashboard). LINE and payment gateway webhooks are a special case — rate-limit them generously, since a burst of legitimate webhook retries after a network blip shouldn't get dropped alongside actual abuse.
Should You Use Middleware?
Next.js Middleware runs on the Edge before your Route Handler executes. Use it for cheap, universal checks: is this request to /admin coming from an authenticated session, is this IP on a blocklist, does this request need to be redirected based on the Accept-Language header for a bilingual Thai/English site.
Keep it lightweight — Edge Runtime doesn't support Node.js APIs like the filesystem or most database drivers. For anything heavier, like verifying a LINE webhook signature against the channel secret (which needs a Node crypto call in some setups) or checking a payment gateway signature, do that inside the Route Handler itself rather than middleware.
How Do You Handle Webhooks Specifically?
Webhook routes deserve their own checklist, because a LINE OA integration or a PromptPay payment confirmation is effectively an API endpoint the public internet can hit with a valid-looking signature:
- Verify the signature before parsing the body as trusted data — LINE sends an
x-line-signatureheader computed as an HMAC-SHA256 of the raw body against your channel secret; verify it before youJSON.parseanything. - Respond
200quickly, even before finishing processing. LINE and most payment gateways retry aggressively on non-200 responses or timeouts, and a slow handler can end up processing the same event multiple times. - Make webhook handlers idempotent. Store the event ID and skip reprocessing if you've seen it before — a duplicate PromptPay confirmation should not double-credit an order.
- Log the raw payload somewhere durable before you touch it. When a payment dispute comes up weeks later, the raw webhook body is the source of truth.
What Are Common Mistakes to Avoid?
- Returning sensitive detail in error messages — a stack trace or raw SQL error leaking into a booking form's error toast
- Missing
awaitonrequest.json(), which fails silently in a way that's painful to debug in production - Skipping signature verification on a webhook because it "worked in testing" — testing traffic doesn't include an attacker replaying a captured payload
- Letting database queries run without timeouts in serverless functions, so one slow query holds a function instance open and starves concurrent requests
These patterns — strict validation, consistent errors, rate limiting, signature-checked webhooks — are the same ones that hold up whether the route serves a clinic's booking widget, a restaurant's LINE ordering flow, or an admin dashboard nobody outside the business ever sees.
Ready to Build Something Fast?
Get a free quote. We reply within 24 hours.
Ready to build something fast and scalable?
Get a free project quote. We reply within 24 hours.
Get a Free Quote →