Welcome 👋 We’re at the very beginning of building a production-grade e-commerce backend with Next.js and PostgreSQL. In this lesson, you won’t connect to a database or implement business logic yet. Instead, you’ll lay down the structural rules that every API endpoint in this project will follow.
By the end of this lesson, you’ll understand how our backend defines a strict API contract, how we enforce that contract with reusable response helpers, how to safely parse request bodies, and how routing works in the Next.js App Router. These foundations might seem simple, but they’re what keep large systems consistent and predictable as they grow.
Since this is the first lesson in the course path, we’re starting from a clean slate and building our shared language from scratch.
Every backend needs a contract — a shared agreement about what responses look like. If one endpoint returns raw JSON, another returns { result: ... }, and a third returns { data: ... }, your frontend becomes fragile and inconsistent.
In this project, that contract lives in src/lib/types/api.ts. This file contains only TypeScript types — no runtime logic — but it defines the structure that every route must follow.
Let’s build it piece by piece.
First, we define the allowed error codes.
ApiErrorCodeis a narrow union type. This means only these exact string values are allowed — nothing else. You cannot accidentally return"NotFound"or"bad_request"because TypeScript will reject it.- This constraint matters because
src/lib/http/response.tsimports and depends on this type. If the contract becomes too loose, you lose compile-time safety. If it’s incorrect, the app won’t compile cleanly. - By centralizing error codes here, we ensure the entire backend speaks the same error vocabulary.
Next, we define the shape of successful responses.
ApiSuccess<T>is generic, which means it can wrap any payload type. Whether you return a product list or an order object, it always lives underdata.
Types alone are not enough. Developers can still forget to follow them. That’s why src/lib/http/response.ts centralizes how responses are created.
This file ensures every route returns properly shaped JSON.
Let’s start with a small but important utility: normalizeInit.
normalizeInitallows route handlers to pass either a numeric status code (like200) or a fullResponseInitobject.- If a number is provided, it converts it into
{ status: number }. This keeps route handlers clean and concise. - Without this helper, every route would need to manually wrap numeric status codes inside objects.
Now let’s look at the success helper.
success<T>constructs a properly shapedApiSuccess<T>object every time. This ensures the response always hasdataand a timestampedmeta.
When building POST and PUT endpoints, you’ll often call req.json(). But if the request body is invalid JSON, it throws an error.
Instead of wrapping every route in try/catch, we implement a reusable helper: parseJson.
parseJsonattempts to parse the request body usingreq.json().- If parsing succeeds, it returns
{ ok: true, value }, allowing the route handler to safely use the parsed data. - If parsing fails, it does not throw. Instead, it returns a structured failure object with
code: 'VALIDATION_ERROR'. - This design forces route handlers to explicitly handle both outcomes, which improves clarity and avoids hidden runtime crashes.
This helper becomes especially important when implementing the debug route’s POST handler.
To explore how routing works in the App Router, we use a catch-all route located at:
src/app/api/debug/[...slug]/route.ts
The [...slug] syntax means this route captures any number of path segments as an array.
Let’s look at the GET handler.
- In the App Router, route parameters are provided as a Promise, so we must
await paramsbefore using them. resolvedParams.slugcontains an array of path segments, such as["hello", "world"]for/api/debug/hello/world.- We extract query parameters using
req.nextUrl.searchParamsand convert them into a plain object. - The response is wrapped using
success, ensuring it follows our API contract.
Now let’s implement the POST handler using parseJson.
To test endpoints without external tools, the project includes a client-side Playground at:
src/app/playground/page.tsx
This is a "use client" React component that allows you to send requests and inspect responses directly in the browser.
Here’s the small helper that sends requests:
- This helper calls
fetch, reads the raw text response, and attempts to parse it as JSON. - It returns a normalized shape containing
ok,status, and parseddata. - This makes it easy to inspect whether your API contract is being followed.
The Playground UI includes controls for:
- Calling
GET /api/debug/... - Calling
POST /api/debug/... - Calling
GET /api/products
It simply renders whatever JSON the backend returns, making response envelopes and status codes immediately visible.
In this lesson, you established the structural foundation of the backend:
- You defined a strict API contract in
src/lib/types/api.ts. - You enforced that contract with reusable helpers in
src/lib/http/response.ts. - You implemented safe JSON parsing with
parseJson. - You built a catch-all debug route that exercises routing, params, and structured responses.
- You verified everything using the Playground client.
From this point onward, every feature you build — products, carts, orders, inventory — will rely on this exact contract. And because we’ve made it strict, centralized, and type-safe, the system can grow without becoming chaotic.
