First Steps and Foundations

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.

Defining a Strict API Contract

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.

  • ApiErrorCode is 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.ts imports 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 under data.
Enforcing the Contract with Response Helpers

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.

  • normalizeInit allows route handlers to pass either a numeric status code (like 200) or a full ResponseInit object.
  • 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 shaped ApiSuccess<T> object every time. This ensures the response always has data and a timestamped meta.
Safe JSON Parsing Without try/catch Everywhere

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.

  • parseJson attempts to parse the request body using req.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.

Understanding Routing with a Catch-All Debug Route

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 params before using them.
  • resolvedParams.slug contains an array of path segments, such as ["hello", "world"] for /api/debug/hello/world.
  • We extract query parameters using req.nextUrl.searchParams and 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.

Interacting with the API Through the Playground

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 parsed data.
  • 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.

Recap

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.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal