Platforms, Microservices, DevOps and APIs · Insper

API

A contract that lets each side evolve on its own

Replace everything behind it

The team rewrites the product service: new language, new database, new framework.
Which clients break?

  • None — as long as the contract is respected.
  • An API defines the operations, their inputs and outputs — and hides the implementation.
  • In the hands-on: module account = the contract (AccountController + AccountIn/AccountOut); account-service = the private implementation.
  • Changing a DTO is changing the contract: never return the JPA model.

Types of APIs

Style Protocol Format Typical use
REST HTTP/1.1, HTTP/2 JSON / XML public and internal web APIs — the default
gRPC HTTP/2 Protocol Buffers fast internal service-to-service calls
GraphQL HTTP JSON client picks the fields; Backend for Frontend
WebSocket HTTP Upgrade, then full-duplex any real time: chat, live feeds
Message-based AMQP / Kafka JSON / Avro asynchronous, event-driven integration

Same question, three shapes: GET /products/42 · rpc GetProduct(GetProductRequest) · query { product(id: "42") { name price } }

How RESTful? The Richardson model

Level Characteristic Example
0 — Swamp of POX one endpoint for everything POST /service?op=getOrder
1 — Resources one URI per resource GET /orders/42
2 — HTTP verbs right methods + status codes DELETE /orders/42 → 204
3 — Hypermedia responses link to next actions (HATEOAS) "rel": "cancel"

Most production APIs live at level 2 — the focus of this course, documented with OpenAPI / Swagger.

REST: resources, representations, constraints

A resource has a URI (/products/42); clients exchange representations of it (JSON).

Constraint Meaning
Client-Server UI and storage are separate concerns
Stateless each request carries everything — the JWT goes with every call
Cacheable responses declare whether they can be cached
Uniform Interface URIs + standard methods + self-descriptive messages
Layered System the client cannot tell the server from a gateway or load balancer
Code on Demand optional: the server may send code (JavaScript)

Resources are nouns

✅ Good ❌ Bad
GET /orders GET /getOrders
POST /orders POST /createOrder
GET /orders/42 GET /order?id=42
DELETE /orders/42 POST /deleteOrder/42
  • Plural nouns: /orders, /products
  • Lowercase with hyphens: /order-items
  • Nest for ownership: /users/7/orders
  • At most 2–3 levels of nesting
  • Filters, sort, pages → query parameters

HTTP methods

Method Idempotent Safe Use
GET ✅ ✅ read a resource or collection
POST ❌ ❌ create — the server assigns the ID
PUT ✅ ❌ replace the whole resource
PATCH ❌ ❌ partial update
DELETE ✅ ❌ remove
  • Safe: does not change server state. Idempotent: repeating it has the same effect as calling once.
  • A second DELETE /products/42 returns 404 — still idempotent: the state is the same.

Retrying a POST: idempotency keys

POST /orders times out. Was the order created? Retry and risk a duplicate, or give up and risk losing it?

  • The client sends a unique Idempotency-Key (a UUID) per logical operation.
  • The server stores processed keys with their result.
  • A retry with the same key returns the stored result — no duplicate.
  • Same mechanism as Stripe's payment API; retries and backoff → Resilience Patterns.

Status codes: use the right one

2xx — success

  • 200 OK
  • 201 Created + Location
  • 202 Accepted (async)
  • 204 No Content

4xx — client error

  • 400 malformed request
  • 401 no / invalid token
  • 403 valid token, no permission
  • 404 not found
  • 409 conflict, duplicate
  • 422 business rule failed
  • 429 rate limited

5xx — server error

  • 500 unhandled — no stack traces
  • 502 bad answer behind the gateway
  • 503 down or overloaded
  • 504 no answer behind the gateway

401 means "who are you?"; 403 means "I know who you are, and the answer is no".

Request and response

POST /products HTTP/1.1
Content-Type: application/json

{
  "name": "Tomato",
  "price": 10.12,
  "unit": "kg"
}
HTTP/1.1 201 Created
Location: /products/0195abfb-...

{
  "id": "0195abfb-...",
  "name": "Tomato",
  "price": 10.12,
  "unit": "kg"
}

Spring: ResponseEntity.created(uri).body(out) sets both 201 and Location; @ResponseStatus(CREATED) cannot set the header.

Errors with a standard shape: Problem Details

RFC 9457, media type application/problem+json:

{
  "type": "https://store.example.com/problems/out-of-stock",
  "title": "Product out of stock",
  "status": 422,
  "detail": "Product 101 is out of stock",
  "instance": "/orders"
}

In Spring Boot: @RestControllerAdvice + @ExceptionHandler returning a ProblemDetail; spring.mvc.problemdetails.enabled: true for the framework's own errors.

Pagination and versioning

Never return unbounded collections

  • Offset: ?page=2&size=20 — simple, slow on large tables
  • Cursor: ?after=eyJpZCI6NDJ9 — uses the index, no page drift
  • Filter on the server: ?status=PENDING
  • Spring Data pages are 0-based

Version on breaking changes

  • URI path: /v1/orders — most common, easy to test
  • Header: Accept: application/vnd.myapi.v2+json
  • Query param: ?version=2 — avoid, it is not a filter
  • Run v1 and v2 side by side, then retire v1

Breaking or not?

Backward compatible Breaking → new version
add an endpoint remove or rename an endpoint
add an optional request field add a required request field
add a response field remove or rename a response field
accept a new input value change a type or meaning (cents vs. reais)

Clients are tolerant readers: they ignore fields they do not know. Prefer compatible changes; version only when you must.

OpenAPI: the contract as a document

openapi: 3.1.0
info: { title: Product Service API, version: 1.0.0 }
paths:
  /products/{id}:
    get:
      summary: Find a product by ID
      responses:
        "200":
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProductOut" }
        "404": { description: Product not found }
components:
  schemas: { ProductOut: { type: object } }
  • Code-first (this course): springdoc generates it from the Java code. Contract-first: write the YAML, generate code.
  • Swagger = the old name of the spec; today the tools (Swagger UI, Editor).

springdoc and Swagger UI

  • Dependency org.springdoc:springdoc-openapi-starter-webmvc-ui — 3.0.x for Spring Boot 4, 2.8.x for Boot 3.
  • /swagger-ui.html (interactive) · /v3/api-docs (JSON) · /v3/api-docs.yaml
  • Describe with @Tag, @Operation, @ApiResponse, and @Schema(description, example) on the DTO records.
  • JWT: a bearerAuth security scheme in the OpenAPI bean → Authorize button.
  • In our platform the gateway resolves the JWT; test a service directly with the id-account header.
# application-prod.yml
springdoc:
  api-docs: { enabled: false }
  swagger-ui: { enabled: false }

Swagger UI in production documents your entire attack surface.

Takeaways

  1. An API is a contract: consumers depend on it, never on the implementation (account vs account-service).
  2. Most APIs live at Richardson level 2: resources + HTTP verbs + status codes.
  3. Know what is safe (GET) and idempotent (GET, PUT, DELETE); make POST retry-safe with an idempotency key.
  4. Return the right status code and errors as Problem Details.
  5. Paginate collections; prefer compatible changes, version breaking ones with /v2/.
  6. An OpenAPI document describes the contract; springdoc generates it and Swagger UI from the code — keep it off in production.

Outline: (1) what an API contract is, in our platform; (2) API styles and the Richardson model; (3) REST: resources, methods, idempotency keys, status codes, Problem Details, pagination, versioning; (4) OpenAPI documents and Swagger UI with springdoc.

Stateless + layered is what lets Nginx send each request to any gateway replica (hands-on 7).

Ask: a client times out on POST /orders. Can it simply retry? Why is PUT different?