NodeJSDesigning API Endpoints

Designing API Endpoints

Good endpoint design is mostly naming discipline. A consistent, predictable URL scheme means clients can guess endpoints correctly and your API documents itself. This page covers the conventions the industry has converged on: plural nouns, nesting for relationships, when to nest vs. flatten, and how to handle the operations that don't fit neatly into CRUD.

Use plural nouns for collections

Good

Avoid

Why

GET /users

GET /user

A collection is plural

GET /users/5

GET /users/get/5

No verb in the path

POST /users

POST /users/create

Method is the verb

GET /users/5/orders

GET /userOrders?id=5

Model the relationship

Pick plural and stay consistent
The dominant convention is **plural** collection names (`/users`, `/products`), with an id selecting one member (`/users/5`). Mixing singular and plural across an API is the most common consistency failure. Decide once, apply everywhere — predictability matters more than which choice you make.
Nesting for relationships

Text
GET    /users/5/orders        → orders belonging to user 5
POST   /users/5/orders        → create an order for user 5
GET    /users/5/orders/9      → a specific order of user 5
GET    /articles/42/comments  → comments on article 42
Don't nest more than one level deep
Nesting expresses ownership (`/users/5/orders`), but `/users/5/orders/9/items/3/tax` is unusable. Cap nesting at **one level**. Once a sub-resource has its own id, prefer addressing it directly: `/orders/9/items` instead of `/users/5/orders/9/items`. Deep nesting couples URLs to a hierarchy that often changes — flatter URLs age better.
Filtering, sorting, paging go in the query string

Text
GET /products?category=books&minPrice=10     ← filter
GET /products?sort=-price,name                ← sort (- = descending)
GET /products?page=2&limit=20                 ← paginate
GET /products?fields=id,name,price            ← sparse fieldset
GET /users?q=ada                              ← search
Path identifies; query string refines
Reserve the **path** for identifying resources and the **query string** for everything that filters, sorts, paginates, or shapes a collection. `GET /products/cheap` bakes a filter into the path (rigid); `GET /products?maxPrice=10` keeps it flexible and composable. Details in [pagination & filtering](/nodejs/pagination).
Actions that aren't CRUD

Some operations — "publish", "cancel", "send email" — don't map cleanly onto create/read/update/delete. Two acceptable patterns:

Text
// 1. Model the action as a sub-resource state change (preferred):
POST   /articles/42/publish        ← a controller-style action
PATCH  /articles/42  { "status": "published" }   ← or a state field

// 2. Model it as a sub-collection when it creates something:
POST   /orders/9/refunds           ← creating a refund resource
Prefer state changes; allow action endpoints sparingly
When an action just changes a field, model it as a `PATCH` to that field — it stays RESTful. When it genuinely *creates* something (a refund, a shipment), `POST` to a sub-collection. Reserve verb-in-path "action" endpoints (`/publish`, `/cancel`) for the cases neither fits — they're a pragmatic escape hatch, not the default.
URL style rules
  • Lowercase paths — /userProfiles and /userprofiles may differ on some servers; stick to lowercase.

  • Hyphens, not underscores or camelCase, for multi-word paths — /order-items.

  • No trailing slash convention — pick one (usually none) and redirect the other.

  • No file extensions/users, not /users.json; use the Accept header for format.

  • Version at the root/v1/users (see API versioning).

A worked resource: blog posts

Method & path

Action

GET /posts

List posts (paginated, filterable)

POST /posts

Create a post

GET /posts/:id

Fetch one post

PUT /posts/:id

Replace a post

PATCH /posts/:id

Partially update a post

DELETE /posts/:id

Delete a post

GET /posts/:id/comments

List comments on a post

POST /posts/:id/comments

Add a comment to a post

Mapping the design onto an Express router

JS
import { Router } from 'express'
const posts = Router()

posts.get('/',            listPosts)
posts.post('/',           createPost)
posts.get('/:id',         getPost)
posts.put('/:id',         replacePost)
posts.patch('/:id',       updatePost)
posts.delete('/:id',      deletePost)
posts.get('/:id/comments',  listComments)
posts.post('/:id/comments', addComment)

// app.use('/v1/posts', posts)
export default posts
Design first, then wire it up
Sketch the full resource table before writing handlers — it surfaces inconsistencies (a missing `PATCH`, an over-nested route) while they're cheap to fix. The router then mirrors the table line-for-line, which is exactly the [modular routing](/nodejs/express-router) pattern. A clean design makes the implementation almost mechanical.
Next
Implement the full create/read/update/delete cycle: [CRUD Operations](/nodejs/crud-operations).