Standardizing API Responses
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.
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.- The
metafield is optional, but when present, it contains a timestamp string. This gives us a consistent place for metadata like request timing or debugging context. - Wrapping payloads inside
dataprevents ambiguity. The frontend always knows where the real response lives.
Now we define the structure of error responses.
- Instead of returning raw error strings, we wrap errors inside an
errorobject. This keeps failure responses clearly separated from success responses. codeis machine-readable and restricted toApiErrorCode, which keeps the system consistent.messageis human-readable and intended for display or logging.detailsis optional and typed asunknown, which allows flexibility for validation errors or debugging context without locking the API into a rigid shape.
Finally, we define the full API response type.
ApiResponse<T>is a union type. It guarantees that every response is either a success envelope or an error envelope — never both and never raw data.- This union becomes extremely powerful in TypeScript-aware clients because they can narrow the type based on whether
errorexists. - This type formalizes the contract. Every endpoint must conform to it.
At this point, we have defined the language of our backend. Next, we enforce it.
