We closed the previous module with the Spring container completely taken apart: we know how beans are born, how they are injected, where their properties come from and why they show up on their own. All of that happens underneath, where nobody sees it. From now on we work in the layer that is visible: the HTTP API that will be consumed by the CicloUrbana mobile app, the control panel used by Ribalta's operators and, eventually, the city council's open data portal. This first lesson does not write controllers —that starts in the next one—; it sets the frame instead: what REST really means, how resources and URLs are designed, which verb and which status code belong to each operation, how the response format is negotiated and how a public contract is versioned. The decisions we take here shape the six lessons that follow and, in a real system, are very expensive to reverse: a published URL is a promise.

Contents

  1. What REST is and where it comes from
  2. The six REST constraints and what they mean in practice
  3. Resources, identifiers and representations
  4. The Richardson maturity model
  5. Designing CicloUrbana's URLs
  6. The complete API contract for the module
  7. HTTP verbs: safety and idempotence
  8. HTTP status codes
  9. Content negotiation
  10. API versioning
  11. REST versus SOAP, GraphQL and gRPC
  12. Where Spring MVC fits: the DispatcherServlet
  13. Common Mistakes and Tips
  14. Exercises

  1. What REST is and where it comes from

REST —REpresentational State Transfer— is not a protocol, nor a library, nor a format. It is an architectural style described by Roy Fielding in chapter 5 of his 2000 doctoral dissertation, Architectural Styles and the Design of Network-based Software Architectures. Fielding did not invent REST by observing how the APIs of his day were built: he described it by analysing why the Web had worked at a scale no previous distributed system had reached.

That origin explains a great deal. REST is not "JSON over HTTP with pretty URLs". It is a set of constraints that, applied to a distributed system, give it certain desirable properties: scalability, independence between client and server, tolerance to evolution and the ability to slot in intermediaries (caches, proxies, load balancers) that understand the traffic without knowing the application.

The distinction that matters to a developer is this:

Claim True?
REST requires JSON No. REST mentions no format at all. JSON is the usual choice, not a requirement.
REST requires HTTP Not formally, but HTTP is the only relevant implementation and the one we will always assume.
REST requires URLs made of nouns Not literally, but it is the natural consequence of the uniform interface constraint.
An API with clean URLs and JSON is already REST Not necessarily. It usually stops at maturity level 2 (section 4).
REST is faster than SOAP Not by definition. It is simpler and more cacheable, which often translates into speed.

In everyday industry usage, "REST API" is a synonym for "resource-oriented HTTP API with JSON". CicloUrbana will be exactly that, with full awareness of which constraints we meet and which we do not.

  1. The six REST constraints and what they mean in practice

Fielding defines six constraints. Five are mandatory and one is optional. The interesting part is not memorising them but seeing what each one forces upon the code we are about to write.

Client-server

Separation of concerns: the client takes care of the user interface, the server of the data and the business rules. They communicate only through the API contract.

What it means for CicloUrbana: the backend never generates HTML and never knows whether the client is an Android app or a web panel. If tomorrow the council wants a touch kiosk at every station, the server is untouched. It also means that business logic (can you rent a bike with 15% battery?) lives on the server, not in the app: a client may be out of date, or malicious.

Stateless

Every request carries all the information needed to process it. The server keeps no session context between requests.

What it means: no HttpSession holding the user's basket, the wizard step or the id of the rental in progress. If the client needs to authenticate, it sends the credential —a token— on every request (module 5). If it needs to paginate, it sends the page number every time.

The benefit is horizontal scalability: if CicloUrbana grows and we deploy three instances behind a load balancer, any of them can serve any request. With state held in memory we would need session affinity or a replicated session, and both complicate deployment (we will see this when we containerise in 07-04).

The cost is that requests are bigger and work is sometimes repeated (validating the token on every call). It is a trade-off that is almost always worth taking.

Cacheable

Every response must state, explicitly or implicitly, whether it can be cached and for how long.

What it means: GET /api/v1/stations returns data that barely changes —a new station once a month— and can carry Cache-Control: public, max-age=300. GET /api/v1/stations/1/bikes, by contrast, changes every minute and must carry Cache-Control: no-store or a very short max-age. Getting this decision right takes more load off the server than any code optimisation (we will come back to it in 09-02).

Uniform interface

This is the central constraint and the one that most distinguishes REST. It breaks down into four sub-constraints:

Sub-constraint Meaning In CicloUrbana
Resource identification Every thing has its URI /api/v1/stations/1 identifies Main Square
Manipulation through representations The client modifies by sending a representation, not by invoking methods PUT with the station's complete JSON
Self-descriptive messages Every message carries what is needed to interpret it Content-Type: application/json, status codes
HATEOAS The response includes links to the possible transitions Optional; see level 3 in section 4

Layered system

The client does not know whether it is talking to the final server or to an intermediary. Between the app and our Tomcat there may be a CDN, a load balancer, an API gateway and a security proxy, and nothing changes.

What it means: never assume the client's IP without looking at X-Forwarded-For, never trust that the port the application sees is the one the client used, and build absolute URLs carefully (we will see this with the Location header in 03-03).

Code on demand (optional)

The server may send executable code to the client. It is the only optional constraint and it is practically unused in data APIs. CicloUrbana will not apply it.

  1. Resources, identifiers and representations

Three concepts that get confused constantly and are worth separating precisely.

  • Resource: any concept in the domain worth naming. "The Main Square station", "the bikes available at station 3", "rental number 4471". It is an abstract notion, not a row in a table.
  • Identifier (URI): the resource's stable name. /api/v1/stations/1.
  • Representation: a concrete way of showing the resource's state at a given moment. The same resource can have several representations: JSON, XML, CSV, a summary version and a detailed one.
graph LR
    R["Resource<br/>«The Main Square station»"] -->|is identified by| U["/api/v1/stations/1"]
    R -->|is represented as| J["application/json<br/>{ id, name, capacity... }"]
    R -->|is represented as| C["text/csv<br/>1,Main Square,24"]

The practical consequence matters: the resource is not the Java class. Our record Station is an internal representation; what the API exposes is something else, which may omit fields, add computed fields or aggregate data from several sources. That separation is exactly the subject of lesson 03-05 (DTOs).

A concrete CicloUrbana example: the "station" resource in the list view will include availableBikes, a computed number that is not in the record Station. And it will not include the exact coordinates of the maintenance dock, which are in the internal model. Resource and class are different things.

  1. The Richardson maturity model

Leonard Richardson proposed a four-level scale in 2008 to measure how close an API is to REST. It is the most useful tool for diagnosing an existing API.

Level Name What it uses Example in CicloUrbana
0 The swamp of POX One URL, one verb POST /api/service with {"operation":"listStations"}
1 Resources Several URLs, one verb POST /api/stations/list, POST /api/stations/1/delete
2 HTTP verbs URLs + verbs + status codes GET /api/v1/stations, DELETE /api/v1/stations/1 → 204
3 HATEOAS The above + links in the responses The station returns _links.bikes and _links.rent

Let us look at them with concrete requests.

Level 0 — everything goes through a single entry point and the HTTP verb means nothing:

POST /api/service HTTP/1.1
Content-Type: application/json

{ "operation": "getStation", "parameters": { "id": 1 } }

The response will be 200 OK even if the station does not exist, with a {"error": "not found"} in the body. HTTP is used as a plain tunnel. It is the SOAP model and that of many legacy internal APIs.

Level 1 — there are resources with their own URL, but the operations are still verbs in the path:

POST /api/stations/1/get
POST /api/stations/1/delete
POST /api/stations/create

Readability improves, but an intermediary still cannot cache anything or know which requests are safe.

Level 2 — the HTTP verb expresses the operation and the status code expresses the outcome:

GET    /api/v1/stations/1      → 200 OK
DELETE /api/v1/stations/1      → 204 No Content
DELETE /api/v1/stations/999    → 404 Not Found

Now a CDN knows it can cache the GET, a proxy knows it can retry the DELETE without side effects and a generic client understands the outcome without reading the body. This is the level at which the vast majority of professional APIs operate, and the one CicloUrbana will implement.

Level 3 — responses describe what can be done next:

{
  "id": 1,
  "name": "Main Square",
  "availableBikes": 7,
  "_links": {
    "self":   { "href": "/api/v1/stations/1" },
    "bikes":  { "href": "/api/v1/stations/1/bikes" },
    "rent":   { "href": "/api/v1/rentals", "method": "POST" }
  }
}

The idea is powerful: if the station is empty, the rent link simply does not appear and the client does not need to replicate that business rule. Only level 3 fulfils the complete uniform interface constraint, and only it deserves the name REST according to Fielding.

Why CicloUrbana stops at level 2. Level 3 requires clients that navigate links instead of building URLs, and in practice almost no client does: mobile apps carry the routes hard-coded. The cost of maintaining the links does not pay for itself. It is a conscious, informed decision, which is exactly what is expected of an API designer: know level 3, apply it where it adds value —flows with a complex state machine— and not out of dogma. Spring offers spring-boot-starter-hateoas if it is ever needed.

  1. Designing CicloUrbana's URLs

The rules the project will follow, with their rationale:

Plural nouns, never verbs. /stations, not /getStations or /station. The plural is coherent: /stations is the collection and /stations/1 one element of it. Using the singular forces a case-by-case decision and produces inconsistencies.

Lowercase and hyphens to separate words. /api/v1/user-types, not /userTypes or /user_types. Domain names are case-insensitive but paths are not, and the hyphen is the convention of the web.

Hierarchy to express ownership. /api/v1/stations/1/bikes are the bikes of station 1. The practical rule: never nest more than two levels. /stations/1/bikes/42/rentals/7 is unreadable; if you need rental 7, ask for it by its own URL, /rentals/7.

No extensions or format suffixes. No /stations.json. The format is negotiated with headers (section 9).

No trailing slash. /api/v1/stations and /api/v1/stations/ must be the same thing; we choose the first form.

Filters go in the query string, not in the path. /api/v1/stations?minimumCapacity=20&city=ribalta, not /api/v1/stations/minimum-capacity/20. The reason: the path identifies which resource; the parameters modify how it is returned.

Good Bad Why
GET /api/v1/stations GET /api/v1/getStations The verb is already in the HTTP method
GET /api/v1/stations/1 GET /api/v1/station?id=1 An element has its own URI
GET /api/v1/stations?active=true GET /api/v1/stations/active active is not a resource, it is a filter
GET /api/v1/stations/1/bikes GET /api/v1/bikes?station=1 Both are valid; the first expresses ownership better
DELETE /api/v1/stations/1 POST /api/v1/stations/1/delete The HTTP verb is the operation

About the /api prefix: it separates the API from any static content or web page the application might serve on the same domain, and it makes reverse proxy and CORS rules easier. It is such a widespread convention that its absence is surprising.

  1. The complete API contract for the module

This is the goal of module 3. By the end of lesson 03-07, CicloUrbana will expose exactly this:

Verb Path Description Success Lesson
GET /api/v1/stations Listing with filters and pagination 200 03-02
GET /api/v1/stations/{id} Detail of one station 200 03-02
POST /api/v1/stations Create a station 201 + Location 03-03
PUT /api/v1/stations/{id} Full replacement 200 03-03
PATCH /api/v1/stations/{id} Partial update 200 03-03
DELETE /api/v1/stations/{id} Remove a station 204 03-03
GET /api/v1/stations/{id}/bikes Bikes docked at the station 200 03-03
GET /api/v1/bikes Listing of the network's bikes 200 03-03
GET /api/v1/bikes/{id} Detail of one bike 200 03-03
POST /api/v1/bikes Create a bike 201 + Location 03-03
POST /api/v1/rentals Start a rental 201 + Location 03-03
POST /api/v1/rentals/{id}/finish Finish a rental 200 03-03
GET /api/v1/rentals/{id} Detail of one rental 200 03-03

Thirteen endpoints. None carries a verb in the path except finish, which is a state transition and not a CRUD operation; the corresponding section of 03-03 justifies that exception in detail.

Notice something that will become obvious in 03-05: GET /api/v1/stations and GET /api/v1/stations/{id} return different representations of the same resource. The listing returns a summary; the detail additionally includes the list of docked bikes. This is legitimate and very common: one resource, two representations with different levels of detail.

  1. HTTP verbs: safety and idempotence

Two properties defined in RFC 9110 govern what an intermediary may do with each verb.

  • Safe: it does not modify server state. A crawler can walk every GET in an API without breaking anything.
  • Idempotent: executing it N times leaves the system in the same state as executing it once. This allows retrying without fear when the network fails.
Verb Safe Idempotent Request body Use in CicloUrbana
GET Yes Yes No Query stations, bikes, rentals
HEAD Yes Yes No Check existence without downloading the body
OPTIONS Yes Yes No Discover allowed verbs; used by CORS
POST No No Yes Create a station, start a rental
PUT No Yes Yes Replace a whole station
PATCH No Not guaranteed Yes Modify individual fields of a station
DELETE No Yes Optional Remove a station

Idempotence is more practical than it sounds. Picture the CicloUrbana app on the Ribalta underground: the user taps "rent", the POST goes out, the server processes it, and the response is lost because the phone enters a tunnel. The app retries. If the POST is not idempotent, the citizen ends up with two rentals and two charges.

That POST is not idempotent is a feature, not a flaw: each POST /api/v1/rentals creates a new rental, and that is correct. To avoid duplicates caused by retries there is the idempotency key pattern —an Idempotency-Key header carrying a client-generated UUID— that the server remembers so as not to process the same request twice. It is the mechanism payment gateways use. In CicloUrbana we mention it here and do not implement it: it requires persistent storage, which arrives in module 4.

That DELETE is idempotent has a concrete design consequence: DELETE /api/v1/stations/1 on an already deleted station should return 204 or 404, but the final state is the same —the station does not exist—, and that is what defines idempotence. We will come back to it in 03-03.

PATCH is not idempotent in general because its body may describe a relative operation: "increase the capacity by 4" gives a different result every time. If the body describes absolute values —"the capacity becomes 28"— then it is. It is the responsibility of whoever designs the API to decide and document it.

  1. HTTP status codes

The status code is the most ignored and most valuable part of an HTTP response. Returning 200 OK with {"success": false} inside forces every client to read the body to find out whether something worked, and it breaks every intermediary.

The five families:

Family Meaning Whose problem it is
1xx Informational Nobody's; rarely used
2xx Success Nobody's
3xx Redirection The client must go elsewhere
4xx Client error The client's: malformed request, unauthorised, non-existent resource
5xx Server error Ours: a bug, the database down, a dependency unavailable

The 4xx/5xx distinction is not cosmetic. Monitoring systems (module 9) alert on 5xx and not on 4xx, because a 404 is normal operation and a 500 is a phone call at three in the morning. Returning 500 when the client has sent invalid JSON generates noise and erodes trust in the alerts.

The codes CicloUrbana will use:

Code Name When CicloUrbana returns it
200 OK Successful query, PUT/PATCH returning the updated resource
201 Created Station, bike or rental created; always with a Location header
204 No Content Successful DELETE; PUT that returns no body
304 Not Modified Response to a conditional GET with a matching ETag (03-03)
400 Bad Request Malformed JSON or failed validation (03-04)
401 Unauthorized The token is missing or invalid (module 5)
403 Forbidden Authenticated but without permission (module 5)
404 Not Found Station 999 does not exist
405 Method Not Allowed DELETE /api/v1/stations on the collection
409 Conflict Duplicate plate, station full when returning a bike
412 Precondition Failed If-Match with a stale ETag (03-03)
415 Unsupported Media Type XML is sent where JSON is expected
422 Unprocessable Entity Correct syntax but a business rule violated (03-04)
500 Internal Server Error Unhandled exception; must never leak details (03-06)
503 Service Unavailable Dependency down, startup in progress (module 7)

Two frequent confusions worth settling right now:

  • 401 versus 403. 401 means "I don't know who you are" (authentication missing or invalid); 403 means "I know who you are and you may not" (authorisation missing). The standard's names are unfortunate: Unauthorized actually means unauthenticated.
  • 400 versus 422. 400 is "I don't understand your request" (broken JSON, missing mandatory field, wrong type); 422 is "I understand you perfectly, but what you are asking for is not acceptable" (you want to rent a bike that is under maintenance). Lesson 03-04 sets the project's policy.

  1. Content negotiation

HTTP allows the same resource to be served in several formats and lets client and server agree on which one. The mechanism is a pair of headers:

  • Content-Type: describes the format of the body being sent. It is set by whoever sends the body, whether the client in a POST or the server in the response.
  • Accept: is the list of formats the client can interpret, in order of preference. It is always set by the client.
POST /api/v1/stations HTTP/1.1
Host: api.ciclourbana.ribalta.example
Content-Type: application/json
Accept: application/json

{ "name": "Central Market", "capacity": 20 }

Here the client says: "I'm sending you JSON and I want JSON back". The server responds:

HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/stations/5

The MIME types relevant to the course:

MIME type Use
application/json The default format of the whole CicloUrbana API
application/problem+json Error responses with RFC 7807 Problem Details (03-06)
application/x-www-form-urlencoded Classic HTML forms; not used by this API
multipart/form-data File upload (incident photos of the bikes)
text/csv Data export for the city council
application/vnd.ciclourbana.v2+json Versioning by media type (section 10)

The Accept header supports preference weights:

Accept: application/json;q=0.9, text/csv;q=0.5, */*;q=0.1

The client prefers JSON, accepts CSV and, as a last resort, anything. If the server cannot satisfy any of the options, it responds 406 Not Acceptable. If the client sends a Content-Type the server cannot read, the response is 415 Unsupported Media Type. These are two symmetrical codes that are often confused: 406 looks at Accept, 415 looks at Content-Type.

Spring implements all of this automatically from the produces and consumes attributes of @RequestMapping, which we will see in the next lesson.

  1. API versioning

A public API is a contract. As soon as the CicloUrbana mobile app is published in the stores, there will be citizens of Ribalta with old versions installed for months. Renaming a field breaks their phones.

The basic rule is to distinguish compatible from incompatible changes:

Compatible (no new version required) Incompatible (new version required)
Adding an optional field to a response Removing or renaming a field
Adding a new endpoint Changing the type of a field
Adding an optional query parameter Making a previously optional field mandatory
Adding a value to a response enum Changing the meaning of a field
Relaxing a validation Changing a success status code

The three versioning strategies:

By URL — /api/v1/stations, /api/v2/stations.

GET /api/v1/stations HTTP/1.1

Advantages: visible at a glance, trivial to test in a browser or with curl, easy to route in a proxy or to deploy as separate applications, obvious in the logs. Theoretical drawback: two different URLs for the same resource, which breaks the idea of a unique identifier.

By custom header — the client sends X-API-Version: 1.

GET /api/stations HTTP/1.1
X-API-Version: 2

Advantage: the resource's URI is a single one. Drawbacks: invisible in the access log without extra configuration, impossible to test by pasting a URL in the browser, and the HTTP cache needs Vary: X-API-Version so it does not serve the wrong version.

By media type (version content negotiation) — the purist variant.

GET /api/stations HTTP/1.1
Accept: application/vnd.ciclourbana.v2+json

Advantage: theoretically the most correct; the version is part of the representation, not of the identity. Drawbacks: the hardest to explain to a client team, the most awkward to test and the one tooling supports worst.

Strategy Visibility Ease of testing REST purity Real adoption
URL (/api/v1) High High Low Very high
Header Low Medium Medium Low
Media type Low Low High Low

CicloUrbana uses /api/v1. The reasoning: the theoretical advantage of the other two does not offset the operational cost. With the version in the path, an operator looking at Ribalta's logs sees instantly which version each client uses, a new developer understands the scheme in three seconds, and the day v2 arrives we will be able to deploy /api/v2 on another instance and migrate clients gradually. It is the choice of GitHub, Stripe and practically every large-scale public API.

A warning about the number: the API version is not the software version. We can ship CicloUrbana 3.7.2 and keep serving /api/v1. The API version only changes when the contract breaks backwards, and that should happen very few times in a product's life.

  1. REST versus SOAP, GraphQL and gRPC

REST is not the only option. Knowing the alternatives helps you tell when REST is not the answer.

Criterion REST/HTTP SOAP GraphQL gRPC
Format JSON (free) XML mandatory JSON with a query language Binary Protobuf
Contract OpenAPI (optional) WSDL (mandatory) Schema (mandatory) .proto (mandatory)
Transport HTTP HTTP, JMS, SMTP HTTP (usually a single POST) HTTP/2
HTTP caching Native No Hard (everything is POST) No
Over-fetching Frequent Frequent Solved by design Controlled
Human-readable Yes Barely Yes No (binary)
Performance Good Low Good Very high
Bidirectional streaming No No Subscriptions Yes, native
Learning curve Low High Medium Medium

When to choose each, in one sentence:

  • REST: public API, many heterogeneous clients, caching matters, third-party integrations. This is CicloUrbana's case.
  • SOAP: integration with corporate or public-administration systems that already demand it. Chosen out of obligation, not preference.
  • GraphQL: very diverse clients with very different data needs —a mobile app that wants little and a control panel that wants everything— where over-fetching is a real problem.
  • gRPC: communication between internal microservices, where performance matters and you control both ends. We will return to this scenario in 07-06.

They are not mutually exclusive. A mature architecture can expose REST to the outside world and use gRPC between internal services. In module 7, when CicloUrbana is split into services, we will see that contrast live.

  1. Where Spring MVC fits: the DispatcherServlet

All the theory above translates, in Spring Boot, into a single central component. When we added spring-boot-starter-web in module 1, autoconfiguration —which we now know how to read— registered a DispatcherServlet mapped to /. It is the front controller: every request goes through it.

sequenceDiagram
    participant C as Client (Ribalta app)
    participant T as Tomcat + Filters
    participant D as DispatcherServlet
    participant HM as HandlerMapping
    participant HA as HandlerAdapter
    participant CT as StationController
    participant MC as HttpMessageConverter (Jackson)

    C->>T: GET /api/v1/stations/1<br/>Accept: application/json
    T->>D: filter chain and servlet
    D->>HM: which method serves this path?
    HM-->>D: StationController#getById
    D->>HA: invoke it
    HA->>HA: resolves arguments (@PathVariable id = 1)
    HA->>CT: getById(1L)
    CT-->>HA: Station
    HA->>MC: serialise according to Accept
    MC-->>D: {"id":1,"name":"Main Square",...}
    D-->>T: 200 OK + Content-Type: application/json
    T-->>C: HTTP response

The pieces and their roles:

Component Responsibility
DispatcherServlet Orchestrates the whole flow; it is the single entry point
HandlerMapping Decides which method of which controller serves the path (RequestMappingHandlerMapping reads the @GetMappings)
HandlerAdapter Invokes the method, resolving its arguments
HandlerMethodArgumentResolver Turns parts of the request into Java parameters (@PathVariable, @RequestParam, @RequestBody)
HttpMessageConverter Converts between the HTTP body and Java objects; MappingJackson2HttpMessageConverter does the JSON
HandlerExceptionResolver Translates exceptions into HTTP responses (the central topic of 03-06)

One idea is worth nailing down: a Spring controller does not see HTTP. It receives a Long and returns a Station. All the translation —parsing the path, choosing the format, serialising, setting headers— is done by the components in the table. That is why Spring controllers are so compact and so easy to test. When something does not work as you expect, the culprit is almost always one of those intermediate components, and knowing they exist is half of the debugging.

Common Mistakes and Tips

Putting verbs in the URL. POST /api/v1/stations/create gives away a level 1 design. The verb is already in the HTTP method; repeating it in the path is redundant and blocks caching and automatic retries.

Always returning 200. A 200 OK with {"error": "station not found"} forces every client to inspect the body and misleads monitoring, proxies and browsers. The status code is the first line of the response for a reason.

Confusing 401 and 403. 401 = I don't know who you are. 403 = I know who you are and you may not. This gets consolidated in module 5.

Returning 500 because of the client. If malformed JSON arrives, that is a 400. A 5xx means "we failed" and it pollutes production alerts.

Versioning too soon or too late. Publishing /api/v2 because an optional field was added multiplies maintenance cost for no reason: adding fields is compatible. Conversely, renaming a field in /api/v1 without warning breaks clients in production. The table in section 10 is the reference.

Half-hearted pluralisation. /api/v1/stations and /api/v1/bike/1 in the same API. The inconsistency forces you to check the documentation for every endpoint. Pick a convention and never break it.

Tip: write the contract before the code. The table in section 6 was written before a single controller. Arguing over a table takes minutes; renegotiating a deployed API takes weeks.

Tip: think about the client when in doubt. /stations/1/bikes or /bikes?station=1? Ask yourself which one whoever builds the app will write more naturally. If both are useful, expose both: it is not a sin.

Exercises

Exercise 1: Diagnose the maturity level

Ribalta city council hands over the API of its previous bike system. These are four real calls:

POST /bicing/api HTTP/1.1
Content-Type: application/json
{ "action": "listStations", "city": "ribalta" }

POST /bicing/api HTTP/1.1
{ "action": "deleteStation", "id": 3 }

POST /bicing/api HTTP/1.1
{ "action": "getStation", "id": 999 }
→ 200 OK  { "ok": false, "message": "does not exist" }

Determine the Richardson level, list which REST constraints it breaks and rewrite the three calls as a level 2 API with their status codes.

Exercise 2: Design the incident resources

CicloUrbana needs to manage incidents: a citizen reports that bike RB-0142 has a flat tyre; an operator inspects it and closes the report. The data of an incident are: identifier, bike plate, description, opening date, status (OPEN, IN_REVIEW, CLOSED) and assigned operator.

Design the contract: URLs, verbs, success and error status codes, and justify how you model the "close an incident" state transition.

Exercise 3: Decide the versioning policy

The CicloUrbana team proposes four changes for the next release. For each one, decide whether it is compatible or incompatible and what to do:

  1. Add the field availableElectricBikes to the response of GET /api/v1/stations.
  2. Rename capacity to totalCapacity in every station response.
  3. Change latitude and longitude from two loose fields to a nested object {"location": {"lat":..., "lon":...}}.
  4. Accept a new optional parameter ?sortBy=name in the station listing.

Solutions

Solution 1.

Level: 0 (the swamp of POX). There is a single URL (/bicing/api), a single verb (POST) and the operation travels in the body. HTTP is used as a mere tunnel.

Constraints broken:

  • Uniform interface / resource identification: station 3 has no URI. It cannot be linked, bookmarked or cached individually.
  • Uniform interface / self-descriptive messages: 200 OK for a non-existent resource is a lie. No intermediary can interpret the response without knowing the proprietary format.
  • Cacheable: listing stations is a query, but because it travels by POST no cache can store it. All read traffic reaches the server.

Rewrite at level 2:

GET /api/v1/stations?city=ribalta HTTP/1.1
Accept: application/json
→ 200 OK, body: [ {...}, {...} ]
→ 200 OK with an empty array if there are none (that is not an error)

DELETE /api/v1/stations/3 HTTP/1.1
→ 204 No Content   (no body)
→ 404 Not Found    if station 3 did not exist
→ 409 Conflict     if it has active rentals

GET /api/v1/stations/999 HTTP/1.1
→ 404 Not Found
Content-Type: application/problem+json
{ "type": "https://api.ciclourbana.example/errors/resource-not-found",
  "title": "Station not found", "status": 404, "detail": "Station 999 does not exist" }

The error format is RFC 7807, implemented in lesson 03-06. Note that an empty listing is not a 404: the /stations collection exists even when it is empty. A 404 on a listing is only appropriate if the path itself does not exist.

Solution 2.

Incidents are a first-class resource: they have identity, a life cycle and are queried in their own right.

Verb Path Description Success Errors
GET /api/v1/incidents Listing, filterable by ?status=OPEN&bike=RB-0142 200 —
GET /api/v1/incidents/{id} Detail 200 404
POST /api/v1/incidents Open an incident 201 + Location 400, 404 (non-existent bike)
PATCH /api/v1/incidents/{id} Change the description or assign an operator 200 400, 404
DELETE /api/v1/incidents/{id} Delete (administration only) 204 404, 409
GET /api/v1/bikes/{id}/incidents Incidents of one specific bike 200 404

About the opening date and the initial status: they are not accepted in the POST. The date is set by the server with the Clock bean from CommonConfig and the initial status is always OPEN. Accepting data from the client that the server controls is an avenue for manipulation.

About closing an incident, there are two defensible designs:

# Option A: state transition as an action sub-resource
POST /api/v1/incidents/7/close
{ "operatorId": 12, "resolution": "Inner tube replaced" }
→ 200 OK

# Option B: modify the status field
PATCH /api/v1/incidents/7
{ "status": "CLOSED", "resolution": "Inner tube replaced" }
→ 200 OK

Option B is purer —only the resource's state is modified— but it leaves the server the job of detecting that this particular PATCH triggers side effects (notify the citizen, write an audit record, return the bike to service) and it does not validate illegal transitions well. Option A explicitly names a transition of the state machine, is self-documenting, allows different fields to be required for each transition and is authorised separately in module 5.

Recommendation: option A, the same one CicloUrbana will use in POST /api/v1/rentals/{id}/finish. The general rule: if a modification has a name in the language of the business —"close", "finish", "cancel"— and triggers effects beyond changing a field, it deserves its own action endpoint.

Solution 3.

# Change Verdict Action
1 Add availableElectricBikes Compatible Added to /api/v1. An old client ignores it; Jackson discards unknown fields by default
2 Rename capacity to totalCapacity Incompatible Breaks every client that reads capacity
3 Nest the coordinates in location Incompatible Changes the structure; clients fail when reading latitude
4 Optional parameter ?sortBy=name Compatible Added to /api/v1 with a default value that preserves the current ordering

Strategy for changes 2 and 3. Publishing /api/v2 over a rename is disproportionate. The correct pattern is a gradual transition with duplicated fields:

  1. In /api/v1, add totalCapacity and location while keeping capacity, latitude and longitude. The three new ones and the three old ones coexist; this is compatible.
  2. Mark the old ones as deprecated in the OpenAPI documentation (@Schema(deprecated = true), lesson 03-07) and announce the removal date.
  3. Instrument with Actuator (module 7) which clients still read the old fields.
  4. When usage reaches zero or the deadline expires, remove them in /api/v2.

The underlying lesson: most "incompatible" changes can be turned into compatible ones if you accept a few months of duplication. It is almost always cheaper than maintaining two complete versions of the API in parallel. The major version is reserved for genuine redesigns, not for naming changes.

Conclusion

We now have the frame. You know that REST is an architectural style with six constraints —client-server, stateless, cacheable, uniform interface, layered system and code on demand— and, more importantly, what each one forces upon the code you are about to write: no session in memory, explicit Cache-Control, business logic always on the server. You know the Richardson maturity model and why CicloUrbana deliberately settles at level 2. You have separated resource, identifier and representation, the distinction that will justify the DTOs of lesson 03-05. You have the project's URL design rules, the complete table of the thirteen endpoints we will build and the exact meaning of each verb in terms of safety and idempotence, with the example of the phone in the Ribalta underground tunnel so you never forget why it matters. You handle status codes by family and the two classic confusions —401 versus 403, 400 versus 422. You know how to negotiate content with Accept and Content-Type, and why 406 and 415 are not the same thing. You have compared the three versioning strategies and understand why the course chooses /api/v1 despite it not being the purest. And you have placed REST alongside SOAP, GraphQL and gRPC so you know when it is not the answer.

Finally, you have seen the complete journey of a request inside Spring MVC: Tomcat, filters, DispatcherServlet, HandlerMapping, HandlerAdapter, argument resolvers, HttpMessageConverter. That sequence is the map we will use for debugging throughout the module, because when a request does not reach the expected method, or the JSON does not come out as you thought, the culprit is always one of those pieces.

Lesson 03-02, Creating REST Controllers, goes down to the code. We will take @RestController apart, see how routes are mapped and how Spring extracts template variables, query parameters, headers and bodies to turn them into Java arguments; how Jackson transforms a record into JSON and how to control that transformation field by field and globally from application.yml; and when to return the object directly and when to wrap it in a ResponseEntity. By the end we will have a real StationController, with filtered listing, simple pagination and lookup by identifier, tested with curl and with a .http file. That GET /api/v1/stations which used to return a fixed list finally starts behaving like an API.

Spring Boot Course

Module 1: Introduction to Spring Boot

Module 2: Spring Boot Core Concepts

Module 3: Building RESTful Web Services

Module 4: Data Access with Spring Boot

Module 5: Security in Spring Boot

Module 6: Testing in Spring Boot

Module 7: Advanced Spring Boot Features

Module 8: Deploying Spring Boot Applications

Module 9: Performance and Monitoring

Module 10: Best Practices and Tips

© Copyright 2026. All rights reserved