RizTech Academy logo
RizTech Academy
REST APIs Done ProperlyLesson 1 of 530 min

Designing the endpoints before writing them

An API is a contract. Once a front end depends on it, changing it costs work somewhere else — so it is worth thirty minutes of design before the first controller.

Start from what the client needs

Not from your tables. The mistake is generating CRUD endpoints for every model and calling it an API:

GET    /products
GET    /variants
GET    /categories
GET    /cart-items

To render one product page the front end now makes three calls and assembles the result. It has to know how your tables relate, which is your problem, not its.

Start from the screens instead:

Screen Needs
Product list products with cheapest variant, filters, paging
Product detail one product with every variant and its category
Cart cart lines with current prices and stock
Checkout cart total, delivery slots, addresses
Order history orders with items and status

Then design endpoints that serve those:

GET  /api/products?category=staples&page=1
GET  /api/products/:slug
GET  /api/cart
POST /api/cart/items
GET  /api/orders

Five endpoints, one call per screen. One screen, one request is a good target, and it keeps the client from knowing your schema.

The endpoints

Applying the REST conventions from module 1:

Catalogue
GET    /api/categories
GET    /api/products                    list, filtered
GET    /api/products/:slug              one product with variants

Cart
GET    /api/cart
POST   /api/cart/items                  add
PATCH  /api/cart/items/:id              change quantity
DELETE /api/cart/items/:id              remove
DELETE /api/cart                        empty

Orders
POST   /api/orders                      place an order
GET    /api/orders                      the caller's orders
GET    /api/orders/:id
POST   /api/orders/:id/cancel           an action, not a status update

Auth
POST   /api/auth/register
POST   /api/auth/login
POST   /api/auth/logout
GET    /api/auth/me

Admin
POST   /api/admin/products
PATCH  /api/admin/products/:id
PATCH  /api/admin/variants/:id/stock

Five decisions worth defending.

/cart with no id. There is one cart per user and the server knows who is asking. /users/:id/cart invites a caller to pass somebody else's id, and now you need a check that would not otherwise exist. Do not put an id in the URL when the token already identifies the resource — it removes a whole class of authorisation bug.

/orders/:id/cancel rather than PATCH /orders/:id. Cancelling has rules — only some statuses allow it, stock must be restored. A generic status update implies any transition is legal.

/products/:slug not /products/:id. The front end has the slug from the URL; making it look up an id first is a wasted round trip.

Admin under /admin. The separation makes the guard obvious and lets you apply it to the whole prefix.

Nesting one level at most. /cart/items/:id is fine. /users/:id/orders/:id/items/:id is not.

Design the response shape once

Decide now, because changing it later touches everything.

Return the object for a single resource:

{ "id": "clx1", "slug": "atta-5kg", "name": "Aashirvaad Atta" }

Return an envelope for a list, because pagination has nowhere else to go:

{
  "items": [...],
  "page": 1,
  "limit": 20,
  "total": 47,
  "totalPages": 3,
  "hasNext": true
}

Some APIs wrap everything in { "data": ... } for consistency. Both are defensible; pick one and never mix. An API where some endpoints wrap and others do not is exhausting to consume.

Be consistent about field naming. camelCase throughout, since both halves are TypeScript. Never price_paise in one response and pricePaise in another.

Never return internal fields. No passwordHash, no internalNotes, no raw database rows — the rule from the controllers lesson.

Design the resource shape

A product as the client sees it:

// packages/shared/src/product.ts
export interface VariantSummary {
  id: string;
  sku: string;
  label: string;
  unit: Unit;
  pricePaise: number;
  mrpPaise: number | null;
  inStock: boolean;
}

export interface ProductSummary {
  id: string;
  slug: string;
  name: string;
  brand: string | null;
  imageUrl: string | null;
  category: { slug: string; name: string };
  cheapestVariant: VariantSummary;
}

export interface ProductDetail extends ProductSummary {
  description: string | null;
  variants: VariantSummary[];
}

Two decisions here matter.

inStock: boolean rather than stock: 12. The exact count is commercial information you may not want public, and the client only needs to know whether to enable the button. Expose the number only where it is useful — "only 3 left" — and then deliberately.

ProductSummary and ProductDetail are different shapes. A list of forty products should not carry forty descriptions. Being explicit about this in types stops the list endpoint quietly growing.

These live in packages/shared so the front end uses the same definitions — module 9's subject.

Filtering, sorting and paging

Query parameters, consistently named across every list endpoint:

GET /api/products
  ?category=staples
  &q=atta
  &minPaise=10000
  &maxPaise=50000
  &inStock=true
  &sort=price_asc
  &page=2
  &limit=20

Use the same parameter names everywhere. If products use page and orders use offset, every consumer has to remember which is which.

Name the unit in the parameter. minPaise rather than min removes any question about whether to send 500 or 50000.

Write it down before you build it

An OpenAPI document, generated from your code:

npm install @nestjs/swagger --workspace=apps/api
// apps/api/src/main.ts
const config = new DocumentBuilder()
  .setTitle("Kirana Store API")
  .setVersion("1.0")
  .addBearerAuth()
  .build();

SwaggerModule.setup("api/docs", app, SwaggerModule.createDocument(app, config));

/api/docs now serves interactive documentation with every endpoint, its parameters and its response shapes — generated from the DTOs, so it cannot go stale the way a hand-written document does.

Documentation that is generated stays true. Documentation in a wiki does not.

The test of a good design

Could a developer guess the URL for an operation they have not seen?

Given GET /api/products and GET /api/products/:slug, anyone would guess GET /api/orders and GET /api/orders/:id correctly. That predictability is worth more than any individual choice.

The related test: can the front end build a screen with one request? If it needs three, the API is exposing your tables rather than serving the client.

Check your work

Why design from screens rather than tables: table-shaped endpoints force the client to make several calls and understand your schema.

Why /cart has no user id: the token identifies the user, so an id in the URL only creates an opportunity to pass someone else's.

Why /orders/:id/cancel over a status PATCH: cancelling has rules; a generic status update implies every transition is allowed.

Why :slug rather than :id for products: the client already has the slug from the URL, so an id would need a lookup first.

Why inStock rather than stock: the count is commercial information, and the client only needs to know whether to enable the button.

Why summary and detail are different types: a list of forty products should not carry forty descriptions.

Why generated documentation: it is derived from the code, so it cannot drift out of date.

The test of a good API: a developer can guess an endpoint they have not seen, and a screen needs one request.

Practice

  1. List every screen the Kirana Store needs. For each, write the single request that would serve it.
  2. Write the full endpoint table. Justify each path in one sentence.
  3. Design a /users/:id/cart endpoint and then write down every authorisation check it needs that /cart does not.
  4. Define ProductSummary and ProductDetail in packages/shared.
  5. Decide your list envelope shape and write it down. Then check every list endpoint you plan matches it.
  6. Choose stock or inStock and defend the choice.
  7. Install Swagger and browse /api/docs.
  8. Give your endpoint table to somebody and ask them to guess the URL for "update a delivery address". If they guess right, the design works.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship