REST and Resource-Oriented APIs

LESSON

Backend and API Architecture

001 30 min intermediate

REST and Resource-Oriented APIs

The core idea: REST is useful when an API exposes stable resources and uses HTTP semantics honestly, trading server-side convenience for contracts that clients, caches, retries, and operators can reason about.

Core Insight

Imagine a learning platform with endpoints such as /createEnrollment, /completeLesson, and /getCourseDetails. The first version is easy for the backend team because each URL mirrors a server function. The trouble appears later: clients cannot tell which calls are safe to retry, cache behavior is unclear, documentation becomes a list of special actions, and every new workflow adds another custom verb.

REST pushes the design conversation one level earlier. Before naming endpoints, ask what stable things the client is trying to identify over time. In a learning platform, those things might be courses, lessons, enrollments, progress records, certificates, and certificate-generation jobs. Once the resources are visible, HTTP methods and status codes can carry shared meaning: GET reads, POST creates or starts work, PATCH changes part of a resource, and DELETE removes or cancels something.

The misconception to drop is that REST is mainly about pretty URLs or avoiding verbs at all costs. The deeper mechanism is contract design. A resource-oriented API gives clients stable nouns, gives infrastructure recognizable HTTP behavior, and gives the backend a public boundary that can survive internal refactors.

That stability costs something. Designing resources is slower than exposing the controller method you already wrote, and not every workflow fits cleanly into a simple CRUD shape. The goal is not purity. The goal is an API whose behavior is honest enough that a client developer, a cache, a retry loop, and an on-call engineer can make correct predictions from the contract.

Resource Modeling Before Endpoint Naming

A resource is a client-visible thing with identity and lifecycle. It does not have to be a database table, and it should not simply mirror a class name. It is the thing a caller can point at and ask the system to read, create, update, delete, or observe.

For the learning platform, a first pass might look like this:

GET    /courses/{courseId}
GET    /courses/{courseId}/lessons
POST   /courses/{courseId}/enrollments
GET    /users/{userId}/progress/{lessonId}
PATCH  /users/{userId}/progress/{lessonId}

This shape tells a different story from /getCourseDetails or /completeLesson. It says that courses, lessons, enrollments, and progress records are stable concepts. It also says that "completing a lesson" is probably a state change on a progress resource, not just an arbitrary function call.

The backend may still do complex work internally. POST /courses/{courseId}/enrollments might validate eligibility, check payment state, write an enrollment row, emit an audit event, and enqueue a welcome email. REST does not require the client to know those internals. It gives the client a stable public handle for the business action.

The practical test is simple:

If the backend implementation changed tomorrow,
would this resource still make sense to a client?

If the answer is yes, the API is probably closer to a good boundary. If the answer is no, the endpoint may be exposing implementation structure rather than product structure.

HTTP Semantics as Part of the Contract

HTTP methods are not just routing labels. They create expectations about safety, idempotency, caching, and failure recovery.

Useful starting definitions:

Those expectations matter most when something goes wrong. Suppose a mobile client sends a request and times out before receiving the response. Should it retry? If the request was GET /courses/42, retrying is normally safe. If the request was POST /courses/42/enrollments, retrying might create a duplicate enrollment unless the API has an idempotency key or a uniqueness rule that makes the operation repeatable.

A resource-oriented API should therefore make retry behavior part of the design, not an afterthought:

POST /courses/42/enrollments
Idempotency-Key: enroll-user-7-course-42

With a key like this, the backend can recognize a repeated creation attempt and return the original result instead of performing the side effect twice. That small detail connects API design to operational reality: networks fail, clients retry, and contracts must say what repeated work means.

The trade-off is precision versus speed. A team can ship faster by treating every endpoint as a custom POST, but then clients and infrastructure lose the shared semantics that make HTTP predictable.

Worked Example: Course Completion

Start with the naive design:

POST /completeLesson

{
  "userId": "u-7",
  "courseId": "c-42",
  "lessonId": "l-3"
}

This endpoint works, but it hides the resource being changed. It also raises awkward questions. What happens if the client calls it twice? Can the caller read the completion state later? Is completion a command, a progress update, or a separate event?

A resource-oriented design makes the target explicit:

PATCH /users/u-7/progress/l-3

{
  "state": "completed",
  "completedAt": "2026-06-12T10:15:00Z"
}

Now the API says that progress is a resource with state. The client can read it, update it, and reason about the result. If the backend later changes how progress is stored, the public contract can remain stable.

There are still design choices. If completion triggers a long-running certificate calculation, the API might create a job resource instead:

POST /certificate-jobs

{
  "userId": "u-7",
  "courseId": "c-42"
}

GET /certificate-jobs/job-99

This is still resource-oriented. The job is a real thing the client can observe. It is more honest than pretending a long-running workflow is just an immediate field update.

When Resource Orientation Breaks Down

REST becomes harmful when the design forces every action into an unnatural noun. Some operations are commands. Some are workflows. Some are searches. Some are asynchronous jobs. The mature move is to model those concepts explicitly instead of pretending everything is a simple entity update.

Good escape hatches include:

The failure mode is dogma. An API can have noun-shaped URLs and still be confusing if the nouns are fake, the method semantics are dishonest, or the response behavior is inconsistent. Conversely, a command-style endpoint can be acceptable when the command is the real business concept and the contract explains its effects clearly.

The corrective question is:

What does the client need to identify, observe, retry, cache, or evolve independently?

Answer that question first. Endpoint names should follow from it.

Connections

This lesson sets up the next API design contrast: GraphQL. REST is strong when stable resources and HTTP semantics are valuable. GraphQL becomes attractive when several clients need different shapes of the same connected domain data.

It also prepares later lessons on versioning, validation, DTOs, and error handling. Those topics are easier to reason about once the API boundary has clear resources and method semantics.

Resources

Key Takeaways

NEXT GraphQL and Client-Shaped Data