Lesson: Backend Foundations and Your First API Contracts

Welcome 👋 This lesson sets the foundation for everything you’ll build in this backend course. Before we touch databases or business logic, we’ll slow down and establish a shared language for how your API behaves, responds, and communicates errors.

In this lesson, you’ll learn how this Remix backend defines a consistent API response shape, how routes return success and error envelopes, and how Remix routing works for APIs. You’ll also explore a debug endpoint and a stubbed products endpoint that intentionally returns no data yet—but already behaves like a production-ready API.

What This Lesson Is About

Modern backends aren’t just about returning data—they’re about being predictable. Frontends, other services, and even your future self rely on APIs behaving consistently, especially when something goes wrong.

In this project, every API route returns one of two shapes: a success envelope or an error envelope. These envelopes are defined once, reused everywhere, and enforced through helper functions. You’ll see how those helpers work, how Remix routes use them, and how even “unfinished” endpoints still follow the same contract.

What We’re Building and Why Foundations Matter

Throughout this course path, you’re not just building isolated endpoints—you’re assembling a cohesive, production-style e-commerce backend. By the end, the system will support product management, a shopping cart that can hold items and compute totals, and an order lifecycle that moves from creation to payment or cancellation. Each unit adds a slice of capability, and together they form the core backend workflows behind a real online store.

To make the journey easier to visualize, here’s the high-level roadmap of how the pieces connect:

Products → Carts → Orders → Pay/Cancel

  • Products are the source of truth for what can be purchased (think: sku, name, price, currency, status).
  • A Cart collects product selections as items (each item ties a product to a quantity and price snapshot), and the cart maintains computed totals like subtotal/tax/total.
  • An Order is typically created from a cart and has its own items plus a status that reflects business progress (for example, pending → paid, or pending → canceled).
  • Finally, you’ll implement state transitions like paying an order or canceling it, which updates status and timestamps and often introduces “you can’t do that anymore” rules.

Even at a glance, you can see that responses will stop being “just a list” and start becoming structured objects with nested relationships. A cart response, for example, doesn’t only return cart fields—it often includes an items array, each item may include a nested product, and the cart includes a object that summarizes what the system computed. Orders look similar: they include order metadata, status, money fields, and the list of purchased items.

Defining the Shared API Response Types

At the core of this backend is a shared set of TypeScript types that define exactly what an API response looks like. These live in src/lib/types/api.ts and are used throughout the server code.

API response type definitions:

This file defines the canonical shapes for success and error responses, along with a union type that represents “any valid API response”.

  • ApiErrorCode restricts error identifiers to a known set of values. This prevents inconsistent strings like "not_found" or "NotFound" from creeping into the system and makes error handling predictable on the client.
  • ApiSuccess<T> defines the shape of every successful response. All data is wrapped inside a data field, with optional metadata that currently includes a timestamp.
  • ApiError defines a structured error object with a machine-readable code, a human-readable message, and optional details for extra context such as validation errors.
  • ApiResponse<T> is a union type that explicitly states: every API route must return either a success envelope or an error envelope—never raw data and never an ad-hoc shape.
Enforcing the Contract with Response Helpers

Defining types is helpful, but it doesn’t stop someone from accidentally returning raw JSON. To enforce consistency, this project centralizes response creation in helper functions.

These helpers live in src/lib/http/response.ts and are used by every API route.

Normalizing response options:

Before creating responses, the helpers need a small utility to normalize status codes and response options.

  • This function allows route handlers to pass either a numeric status code (like 404) or a full ResponseInit object.
  • By normalizing both into a ResponseInit, the rest of the helpers can stay simple and consistent.
  • This pattern keeps route code readable while still allowing full control over response options when needed.
Creating standardized success responses

The success helper wraps data in the agreed-upon success envelope.

  • Every successful response includes a data field and a meta.timestamp, ensuring consistency across all endpoints.
  • The generic type <T> allows TypeScript to infer the shape of data, which improves type safety in route handlers.
  • Centralizing this logic means that if the API contract changes later, you only need to update it in one place.
Creating standardized error responses

Errors are just as important as successes, and they follow an equally strict structure.

  • The code field uses ApiErrorCode, which enforces a known set of error categories.
  • message is intended for humans, while details can carry structured debugging or validation information.
  • Returning errors through this helper guarantees that clients can always rely on the same shape, regardless of where the error originated.
Safely parsing JSON request bodies

Malformed JSON is a common source of runtime errors. Instead of throwing, this project uses an explicit result-based approach.

  • parseJson never throws. Instead, it returns an object that forces callers to explicitly handle failure.
  • This avoids scattered try/catch blocks in route handlers and makes error paths obvious.
  • You’ll rely on this helper heavily once you start accepting POST and PUT requests with request bodies.
Exploring Remix Routing with a Debug Splat Route

To understand how Remix API routing works, this project includes a debug-only endpoint at app/routes/api.debug.$.ts.

This route demonstrates how splat (*) parameters work and also shows consistent use of the response helpers.

Parsing splat parameters:

The route begins with a small helper that turns the splat parameter into an array of path segments.

  • The $.ts filename tells Remix to capture any remaining path segments into params["*"].
  • This helper normalizes that value into an array, making it easier to work with.
  • Filtering empty segments avoids edge cases like trailing slashes.
Handling GET requests with the loader

The loader echoes back the parsed path segments using the standard success envelope.

  • This demonstrates how loaders handle GET requests in Remix API routes.
  • Even though this is a debug endpoint, it still returns a proper ApiSuccess response.
  • Keeping debug routes consistent reinforces the idea that every endpoint speaks the same API language.
Handling POST requests with the action

The action shows method checks, JSON parsing, and error handling in one place.

  • The method check ensures only POST requests are accepted and returns a structured error otherwise.
  • parseJson is used to safely read the request body without risking runtime crashes.
  • The response includes both the parsed path parameters and the request body, making it ideal for experimentation and learning.
A Stubbed Products Endpoint with a Real Contract

The first “real” API route in the project is app/routes/api.products.ts. For Unit 1, it intentionally does not connect to PostgreSQL.

That’s by design.

Products loader: empty data, real envelope:

  • The endpoint reads the query parameter to establish a pattern that will be reused later.
  • The data returned is an empty array, but it’s still wrapped in a proper success envelope.
  • This shows that even unfinished endpoints should follow the same API contract as finished ones.
Products action: explicit “not implemented” error
  • Unsupported operations return structured errors rather than silent failures.
  • The 501 status code clearly communicates that the endpoint exists but isn’t implemented yet.
  • Database wiring and real product creation will be introduced later, in Unit 3—without changing this contract.
Recap

In this lesson, you focused on foundations rather than features:

  • You defined a shared API contract using ApiSuccess, ApiError, and the ApiResponse union.
  • You enforced that contract with reusable success, error, and parseJson helpers.
  • You explored Remix API routing through a splat-based debug endpoint.
  • You examined a stubbed products endpoint that already behaves like a production API.

With these foundations in place, every endpoint you build next will be predictable, consistent, and easy to evolve—exactly what you want in a growing backend system.

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