Building REST APIs

HTTP Through a Backend Engineer's Eyes

LugemineEelvaade

Vaatad tasuta eelvaate õppetundi.

So far, we have designed the internal structure of our Order Management Backend:

Handler
    ↓
UseCase
    ↓
Domain
    ↓
Repository

We have modeled:

Product

Inventory

Order

OrderItem

CustomerId

OrderStatus

Now we move outward.

How does an external client communicate with our application?

For our current architecture, the answer is:

HTTP
+
JSON
+
REST-style API

But understanding HTTP as a backend engineer is not about memorizing:

GET
POST
PUT
PATCH
DELETE

HTTP is part of the application's observable behaviour.

A useful mental model is:

HTTP is a behavioural contract between the client and the server.

A request communicates things such as:

Which resource am I interacting with?

What operation am I requesting?

Which values do I control?

If I repeat this request,
what effect should I expect?

How will success be represented?

How will expected failure be represented?

An API can technically work while still being confusing, unsafe, inconsistent, or difficult to integrate with.

Good backend engineering treats HTTP semantics as part of the design.


HTTP Is an Application Boundary

Our Domain does not know about HTTP.

For example:

order.cancel();

does not know whether the operation came from:

POST /api/v1/orders/{orderId}/cancel

a test, or some future internal entry point.

The HTTP boundary is responsible for:

receiving the request

understanding transport-level intent

validating transport representation

mapping external data

obtaining authenticated context

invoking the UseCase

translating result/failure

returning an HTTP response

Conceptually:

HTTP Request
    ↓
Handler
    ↓
UseCase
    ↓
Domain
    ↓
Application Result / Failure
    ↓
Handler
    ↓
HTTP Response

HTTP remains at the edge of the application.


Anatomy of an HTTP Request

Consider:

POST /api/v1/orders
Content-Type: application/json
Authorization: Bearer ...

{
  "items": [
    {
      "productId": "59ba787c-6d83-4a21-9bae-19c3d0469bd5",
      "quantity": 2
    }
  ]
}

Several different parts are involved.

Method

POST

communicates the kind of interaction being requested.

URI

/api/v1/orders

identifies the resource or capability being addressed.

Headers

For example:

Content-Type
Authorization
Accept

carry request metadata.

Body

The JSON body carries the Order-creation input controlled by the client.

Each part has a different purpose.


HTTP Methods Communicate Intent

Common methods include:

GET

POST

PUT

PATCH

DELETE

They are not interchangeable labels.

Technically, server code could perform the same Java operation from any method.

But protocol semantics matter.

For example:

GET /api/v1/orders/{orderId}

and:

POST /api/v1/orders/{orderId}/cancel

communicate fundamentally different client intent.

The first reads state.

The second asks the system to change business state.


GET Is for Reading

A typical example is:

GET /api/v1/products

Meaning:

Give me a representation of the Products I am allowed to browse.

Likewise:

GET /api/v1/orders/{orderId}

means:

Give me this Customer-owned Order representation.

A GET request should not normally request business mutation such as:

decrease Inventory

cancel Order

create Payment

change Product price

deactivate Product

Why Mutation Through GET Is Dangerous

Imagine:

GET /api/v1/orders/{orderId}/cancel

actually cancelled the Order.

GET requests may be treated as safe reads by:

browsers

caches

proxies

monitoring systems

link preview systems

crawlers

Infrastructure may issue or repeat GET requests under the assumption that they do not request a business-state change.

Therefore:

Never hide a meaningful business mutation behind GET.


Safe HTTP Methods

A method is safe when the client is not requesting a business-state mutation.

The most important safe method for us is:

GET

Safe does not mean:

absolutely nothing changes anywhere

A GET request may still cause:

access logs

metrics

cache activity

Those are operational side effects.

But the client's requested business operation remains read-only.


Idempotency

Another crucial HTTP concept is:

If the same operation is repeated, does the intended final effect remain the same as performing it once?

That property is called:

idempotency

For example:

Set Inventory available quantity = 20

performed once:

20

and repeated several times:

20

has the same intended final state.

That operation is naturally idempotent.

By contrast:

Create another Order

performed twice may produce:

Order A

Order B

So Order creation is not naturally idempotent.


Idempotency Is About Effect

Idempotency does not require the exact same response every time.

Suppose an idempotent resource-removal operation succeeds once.

A repeated request could potentially return a different response because the resource is already gone.

The important property is:

after one successful application
→ target state reached

after repeated applications
→ target state remains reached

Think about the resulting system effect, not byte-for-byte response equality.


Why Idempotency Matters in Distributed Systems

Networks fail ambiguously.

Imagine:

Client
    ↓
POST /api/v1/orders
    ↓
Server creates and commits Order
    ↓
Response connection breaks

The client sees:

timeout

What does that prove?

Only:

The client did not receive a successful response.

Possible realities include:

request never reached server

request reached server but failed

Order was successfully committed
but the response was lost

If the client blindly retries:

POST /api/v1/orders

the system may create a second Order.

This is why retry behaviour and idempotency must be designed deliberately.


POST

POST commonly represents:

creating a new subordinate resource

triggering a command-like operation

performing an operation that is
not naturally idempotent

Our canonical Order creation endpoint is:

POST /api/v1/orders

The client expresses:

Create an Order containing
these requested Products and quantities.

The server creates a new:

Order

with server-owned identity, ownership, price, time, and lifecycle state.


POST Is Not "Anything That Changes Data"

A weak API might contain:

POST /getProducts

POST /updateProduct

POST /deleteProduct

POST /cancelOrder

Everything becomes POST because implementation is easy.

But then much of HTTP's semantic value is lost.

A client can no longer infer much from:

method + URI

Our API should remain resource-oriented and operation-aware.


PUT

PUT generally represents setting or replacing the state of a resource at a known URI.

The useful property for us is:

repeating the same replacement
should result in the same intended state

Our Inventory API is a good example.

Canonical endpoint:

PUT /api/v1/inventory/{productId}

with a body conceptually like:

{
  "availableQuantity": 20
}

means:

Set the current available quantity for this Product to exactly 20.

Calling it once or repeatedly produces the same intended final quantity:

20

This aligns naturally with our:

SetInventoryUseCase

semantics.


PUT Is Not "Increment"

Suppose instead the request meant:

{
  "increaseBy": 20
}

Then repeated requests would produce:

10 → 30 → 50

That would not be idempotent.

The method name alone does not create idempotency.

The implementation and business operation must honour the contract.


PATCH

PATCH represents partial modification of an existing resource.

Our canonical Product update endpoint is:

PATCH /api/v1/products/{productId}

The request may contain:

{
  "priceCents": 8990
}

Meaning:

Change this Product's current price to 8990 cents while leaving omitted fields unchanged.

Our established Product PATCH semantics are:

name omitted/null
→ unchanged

priceCents omitted/null
→ unchanged

{}
→ accepted no-op

This is a partial update rather than full replacement.


PATCH Is Not Automatically Idempotent

Our Product update:

{
  "priceCents": 8990
}

means:

set priceCents = 8990

Applying it repeatedly results in:

8990

So that particular operation is idempotent in effect.

But imagine:

{
  "increasePriceCentsBy": 1000
}

Then repeated execution would produce:

8990 → 9990 → 10990

That operation would not be idempotent.

Again:

The business meaning determines the effect.

The HTTP method communicates expectations but cannot fix incorrect semantics automatically.


DELETE

DELETE usually communicates:

Remove this resource.

But our Product Domain does not support physical Product deletion.

Our business operation is:

deactivate Product

not:

delete Product

Therefore blindly exposing:

DELETE /api/v1/products/{productId}

could communicate stronger semantics than the business actually provides.

Our canonical API uses:

POST /api/v1/products/{productId}/deactivate

This makes the business operation explicit.


HTTP Method Does Not Replace Domain Meaning

An API could theoretically represent Product deactivation as:

PATCH /api/v1/products/{productId}

with:

{
  "active": false
}

But our Product lifecycle deliberately does not expose arbitrary active-state assignment.

There is no v1 reactivation operation.

So an explicit:

POST /api/v1/products/{productId}/deactivate

better matches:

product.deactivate();

The HTTP contract should preserve server-controlled Domain meaning.


Resource-Oriented Thinking

Our primary API resources are:

products

inventory

orders

Examples:

GET /api/v1/products
GET /api/v1/products/{productId}

POST /api/v1/orders
GET /api/v1/orders
GET /api/v1/orders/{orderId}

These are preferable to RPC-like paths such as:

/getAllProducts

/createNewOrder

/findOrderById

because the HTTP method already carries part of the operation semantics.


URI Identifies the Resource, Not Our Java Architecture

Good:

/api/v1/orders

/api/v1/products

Bad:

/orderHandler/create

/productRepository/findAll

/createOrderUseCase

External clients should not know whether our implementation contains:

Handler

UseCase

Repository

JPA

PostgreSQL

Those are internal implementation details.


Consistent Resource Naming

Our API consistently uses plural resource names where appropriate:

/products

/orders

Inventory is modeled as its own resource collection:

/inventory

Consistency makes the API easier to predict.

The goal is not to endlessly debate singular vs plural naming.

The goal is a coherent contract.


Path Parameters

For a specific Order:

GET /api/v1/orders/{orderId}

The path identifies:

which Order

At the HTTP boundary, the value is a string.

For example:

d2e7b44e-463f-4bbc-b16f-b1c80bc80442

The Handler parses it into:

UUID
    ↓
OrderId

The Domain receives a typed identity rather than HTTP path text.


Query Parameters

Query parameters commonly adjust collection reads.

For example:

GET /api/v1/products?page=0&size=20

Our canonical pagination semantics are:

page
→ zero-based
→ default 0

size
→ default 20
→ minimum 1
→ maximum 100

Query parameters are useful for:

pagination

filtering

sorting

where those features exist.

They should not secretly trigger state-changing business commands.


Never Hide Mutation in a Query Parameter

Avoid:

GET /api/v1/orders/{orderId}?cancel=true

The request appears to be a read while a query parameter secretly changes business state.

Our cancellation operation is explicit:

POST /api/v1/orders/{orderId}/cancel

Headers

HTTP headers carry metadata about the request or response.

Relevant examples include:

Content-Type

Authorization

Accept

request/correlation identifiers

For JSON requests:

Content-Type: application/json

communicates the representation format.

Our v1 API primarily uses:

application/json

We do not need a complicated content-negotiation model.


Authorization Header

Authentication credentials commonly arrive through:

Authorization: Bearer ...

The Handler and Domain should not repeatedly parse this header.

The intended flow is:

HTTP Authorization Header
        ↓
Spring Security
        ↓
Authenticated identity
        ↓
CustomerId / authorities
        ↓
Handler / UseCase

The application receives meaningful identity.

It does not receive raw authentication protocol details unless the security boundary specifically needs them.


HTTP Status Codes Are Part of the Contract

An HTTP response includes more than JSON.

For example:

HTTP/1.1 200 OK

or:

HTTP/1.1 404 Not Found

The status communicates broad outcome semantics before the client interprets the response body.

Using status codes consistently makes integrations easier.


200 OK

200 OK is appropriate when an operation succeeds and returns a representation or result.

For example:

GET /api/v1/orders/{orderId}

may return:

200 OK

with the Order representation.

Likewise our established Product update/deactivation APIs may return 200 with the current Product representation when useful.


201 Created

When a new resource is successfully created:

POST /api/v1/orders

a natural success response is:

201 Created

The server may return the created Order representation.

This communicates more accurately than returning generic 200 for every success.


204 No Content

204 No Content means:

operation succeeded
+
response intentionally has no body

It is useful for APIs whose successful mutation returns nothing.

But:

Never return 204 and also attempt to send a JSON response body.

If the updated representation is useful, use an appropriate body-bearing success response instead.


400 Bad Request

Our canonical API uses 400 for invalid request input.

Examples:

malformed UUID

missing required field

invalid request structure

quantity <= 0

negative priceCents

Our canonical validation Problem Details uses:

VALIDATION_ERROR

with field-level errors where relevant.

Not every business rejection is a 400.


401 Unauthorized

Despite the HTTP name, 401 generally means:

valid authentication
was not established

Examples:

missing credential

invalid token

expired/unacceptable token

Our API maps this to:

AUTHENTICATION_REQUIRED

The operation does not proceed as an authenticated Customer/Admin workflow.


403 Forbidden

403 means the caller may be authenticated but lacks authority for the requested operation.

Example:

CUSTOMER
attempts an Admin-only
Product-management operation

This maps naturally to:

ACCESS_DENIED

Authentication and authorization are different concepts.


401 vs 403

A useful distinction is:

401
→ Who are you?
  cannot be established acceptably
403
→ We know who you are,
  but you cannot perform this operation

Do not treat them as interchangeable.


404 Not Found

For a missing resource:

GET /api/v1/orders/{orderId}

may return:

404 Not Found

with:

ORDER_NOT_FOUND

Our Customer-facing Order API makes an additional deliberate security decision.

If:

Order does not exist

or:

Order exists
but belongs to another Customer

the external result is the same:

404
ORDER_NOT_FOUND

This avoids leaking whether another Customer's protected resource exists.


Resource Ownership and 404

Customer-facing Order reads use authenticated ownership scope.

For example:

GET /api/v1/orders/{orderId}

means:

Give me this Order if it is one of my Orders.

It does not mean:

Tell me whether any Customer has an Order with this ID.

This is why 404 is the correct canonical result for both missing and non-owned Orders in our API.


409 Conflict

A request may be structurally valid but incompatible with current business state.

Examples:

cancel a PAID Order

pay a CANCELLED Order

insufficient Inventory

These are not malformed JSON.

They are business-state conflicts.

Our canonical mappings include:

PRODUCT_NOT_ORDERABLE
→ 409

INSUFFICIENT_INVENTORY
→ 409

ORDER_NOT_CANCELLABLE
→ 409

ORDER_NOT_PAYABLE
→ 409

Why We Are Not Using Every Possible 4xx Code

Some APIs use:

422 Unprocessable Content

for certain semantic-validation failures.

Our goal is not to maximize status-code variety.

Our goals are:

clarity

consistency

predictability

We already have a canonical mapping model for this course.

Handlers should follow it rather than choosing statuses ad hoc.


5xx Means Server-Side Failure

Broadly:

5xx

means the server failed to fulfill an otherwise valid operation because of an internal problem.

Examples:

unexpected bug

database unavailable

broken persistence integrity

unhandled infrastructure failure

An expected Domain rejection does not become 500 simply because Java represented it with an exception.


Exception Does Not Mean 500

For example:

OrderNotCancellable

is an expected business rejection.

The Domain expresses the business failure.

The API boundary translates it into:

409

ORDER_NOT_CANCELLABLE

Unexpected failures map to:

500

INTERNAL_ERROR

This separation keeps HTTP concerns outside the Domain.


Structured Error Responses

We do not want random responses such as:

"bad request"

from one endpoint and:

"something went wrong"

from another.

Our canonical API uses an RFC 7807-style Problem Details contract.

For example:

{
  "type": "https://api.tarkev.com/problems/validation-error",
  "title": "Validation failed",
  "status": 400,
  "detail": "One or more fields are invalid.",
  "instance": "/api/v1/products",
  "code": "VALIDATION_ERROR",
  "errors": [
    {
      "field": "name",
      "code": "REQUIRED",
      "message": "Name is required."
    }
  ]
}

The exact mapping implementation comes later in this module.

The important point is:

HTTP status
+
stable application error code
+
structured response

work together.


Never Return Internal Exceptions to Clients

Bad:

{
  "error": "org.hibernate.exception.ConstraintViolationException..."
}

Clients should not receive:

SQL

stack traces

internal Java class names

database implementation details

secret configuration

API errors should communicate safe, actionable client-facing meaning.


Never Return 200 OK for Every Failure

Weak:

200 OK

with:

{
  "success": false,
  "error": "Order not found"
}

Now the client must ignore normal HTTP outcome semantics and inspect a custom field for every operation.

Better:

404
+
ORDER_NOT_FOUND
+
Problem Details body

Use both protocol semantics and structured application error information.


Status Code Alone Is Not Enough

A 400 only tells the client broadly that the request is invalid.

For validation, the client may also need:

error code

human-readable detail

specific field

field-level code

That is why our Problem Details representation includes:

code

errors[]

where appropriate.


HTTP Contract Is Not the Domain Model

Our Domain uses:

OrderStatus.UNPAID

The API may expose:

{
  "status": "UNPAID"
}

But that representation belongs to the DTO/API contract.

The Domain does not need Jackson annotations or HTTP concerns simply to make this JSON work.


Server Authority Must Be Preserved

Consider Create Order.

The client controls:

ProductId

quantity

The server controls:

CustomerId

OrderId

OrderStatus

unitPriceCents

createdAt

totalCents

That distinction must be visible in the HTTP contract.

Bad request design:

{
  "customerId": "another-customer",
  "status": "PAID",
  "totalCents": 100,
  "items": [
    {
      "productId": "...",
      "quantity": 2,
      "unitPriceCents": 1
    }
  ]
}

The client must not control those values.


Clients Express Intent

A better API lets clients say:

Create this Order.

Cancel my Order.

Pay my Order.

The server decides:

Who owns it?

What does it cost?

Is the transition legal?

Can Inventory be consumed?

Has Payment succeeded?

What state should be persisted?

This aligns HTTP with our Domain architecture.


Explicit Business Commands

Some operations map naturally to ordinary resource CRUD-like behaviour.

For example:

create Product

read Product

update Product

Other operations are meaningful state transitions:

deactivate Product

cancel Order

pay Order

Trying to expose every operation as generic field mutation can make the API less accurate.


Order Cancellation

Our canonical cancellation endpoint is:

POST /api/v1/orders/{orderId}/cancel

This means:

Attempt the business operation of cancelling this Customer-owned Order.

Compare with:

PATCH /api/v1/orders/{orderId}

and:

{
  "status": "CANCELLED"
}

The second representation suggests the client can directly assign Order status.

But in our Domain:

order.cancel();

contains business rules.

So the explicit command endpoint better preserves that meaning.


Payment

Our canonical Payment command is:

POST /api/v1/orders/{orderId}/pay

This means:

Attempt to pay this Order.

It does not mean:

set Order status = PAID

The client cannot simply request:

{
  "status": "PAID"
}

because Payment requires:

authenticated ownership

Order eligibility

PaymentService

external provider result

confirmed successful outcome

current Order-state recheck

Only then may application code call:

order.markPaid();

Command vs State Assignment

This distinction is important.

Set status to PAID

is raw state assignment.

Pay this Order

is a business command.

Likewise:

Set status to CANCELLED

is weaker than:

Cancel this Order

HTTP should communicate business intent rather than exposing arbitrary Domain mutation.


Repeating POST /orders

Without a deliberate idempotency mechanism:

POST /api/v1/orders

sent twice may create:

two separate Orders

even if the request bodies are identical.

The API must not pretend otherwise.

If retry-safe Order creation becomes a requirement, we would introduce an explicit idempotency contract.

We do not invent one silently.


Payment Is More Sensitive

Repeated:

POST /api/v1/orders/{orderId}/pay

can have financial consequences.

Our Domain already prevents:

PAID → PAID

through:

order.markPaid();

But that does not guarantee the Payment provider was not called twice.

External Payment idempotency requires dedicated application/provider design.

HTTP method choice alone does not solve it.


Idempotency Is a System Property

Saying:

PUT is idempotent

does not make implementation magically correct.

If the Handler increments something on every identical PUT, the endpoint violates the expected semantics.

Likewise:

POST is normally non-idempotent

does not prevent an application from deliberately implementing idempotency using an idempotency key.

Protocol semantics and implementation must agree.


HTTP Timeout Does Not Mean the Operation Failed

This is one of the most important distributed-system ideas to understand.

Suppose:

Client
    ↓
POST /api/v1/orders
    ↓
timeout

The client knows:

I did not receive the success response.

It does not know:

The Order was definitely not created.

The server may already have committed successfully.

This is why retries must be designed based on operation semantics, not assumptions.


Error Response vs Lost Response

Case A:

server returns 400

client receives 400

The result is known.

Case B:

server commits Order

response is lost in network

client sees timeout

The business operation may already have succeeded.

These cases are fundamentally different.


HTTP and Caching

GET responses may participate in caching depending on headers, proxies, or infrastructure.

We do not need to implement caching in this module.

But this reinforces why GET should accurately represent read semantics.

Protocol infrastructure uses the method as part of its behaviour.


Avoid GET Request Bodies

Even if a particular stack technically supports a body on GET, do not build ordinary resource-query APIs around it.

Use:

path parameters

query parameters

for normal GET input.

Prefer conventional HTTP patterns unless a strong requirement says otherwise.


HTTP Is Stateless at the Interaction Level

A REST-style request should contain or reference enough information for the server to perform the operation without depending on hidden conversational state inside one application process.

For example:

Request 1
→ create Order

Request 2
→ pay Order

The second request identifies:

OrderId

authenticated caller

through explicit application/request state.

It must not depend on:

the same JVM remembering
an Order object from Request 1

Durable state belongs in PostgreSQL.

Authentication context comes with the request.


Never Store Per-Customer Request State in Singleton Beans

Bad:

@Component
public class OrderHandler {

    private Order currentOrder;
}

Spring application components are normally shared across many requests.

One Customer's business state must not be stored in shared mutable Bean fields.

Use:

method-local values

Domain objects

persistent database state

instead.


HTTP Request Boundary Is Not the Database Transaction Boundary

An HTTP request and a database transaction are related but different concepts.

For example:

POST /api/v1/orders
        ↓
CreateOrderHandler
        ↓
CreateOrderUseCase

may execute the local Order creation workflow transactionally.

But HTTP itself does not provide atomicity.

Transaction boundaries belong to the application workflow.


Observable Contract

Internally, one request may involve:

multiple classes

several Repository operations

a transaction

Domain construction

The client should not need to know those details.

The client observes:

method

URI

headers

request body

status code

response body

That is the public API contract.


Internal Refactoring Should Not Rewrite the Public Contract

Suppose we later optimize:

Repository implementation

query strategy

Domain persistence mapping

If Product behaviour has not changed, the client should not need an unnecessary API migration.

Keeping transport and implementation separate gives us this flexibility.


API Design Is Product Design

Consider cancellation.

We must decide:

Which Orders can be cancelled?

How does a client request cancellation?

What means success?

What happens if Order is already PAID?

What happens if Order belongs to another Customer?

Those are not merely Spring Controller details.

They are product behaviour exposed through an API.


Do Not Design Endpoints From Repository Methods

Bad reasoning:

OrderRepository.save()
→ POST /saveOrder

or:

ProductRepository.findAll()
→ GET /findAllProducts

Persistence implementation should not define the public contract.

Start from:

resources

business capabilities

use cases

Do Not Design Endpoints Around Java Class Names

Avoid:

/createOrderUseCase

/orderHandler

/productEntity

Public vocabulary should remain:

/orders

/products

/inventory

Internal architecture is not part of the client-facing API.


Predictability Matters

If:

GET /api/v1/products/{productId}

retrieves one Product, then:

GET /api/v1/orders/{orderId}

should follow a similar resource pattern where appropriate.

Consistency reduces documentation burden because clients can predict common behaviour.


Consistency Beats Cleverness

Avoid an API where:

Order cancellation
→ /orders/{id}/cancel

Product deactivation
→ /products?action=disable

Inventory update
→ /updateInventory

without a deliberate reason.

Use a coherent vocabulary across the API.

Our canonical surface does that.


HTTP Does Not Replace Business Documentation

Even a conventional endpoint:

POST /api/v1/orders

does not tell a client that:

duplicate ProductIds are rejected

Products must exist

Products must be active

server-side current price is authoritative

Inventory is consumed at Order creation

all Items succeed atomically

new Order status is UNPAID

These rules still need to be documented through:

OpenAPI

API documentation

product requirements

HTTP semantics provide structure.

They do not describe the entire business contract.


Create Order Contract

Conceptually:

POST /api/v1/orders

Request:

{
  "items": [
    {
      "productId": "59ba787c-6d83-4a21-9bae-19c3d0469bd5",
      "quantity": 2
    }
  ]
}

The server determines:

CustomerId
→ authenticated caller

OrderId
→ generated UUID

unitPriceCents
→ current authoritative Product price

status
→ UNPAID

createdAt
→ application Clock

totalCents
→ derived from OrderItems

Successful creation:

201 Created

This is far more meaningful than:

POST /createOrder

returning 200 for every outcome.


Browse Products Contract

Conceptually:

GET /api/v1/products

This is a safe read.

The Customer-visible result follows the business rule:

Product active = true
AND
Inventory availableQuantity > 0

The client does not need to know whether the backend used:

join

projection

multiple internal objects

The API returns the representation produced by the business query.


Own Order History

Canonical endpoint:

GET /api/v1/orders

For an authenticated Customer, this means:

Return my Order history.

Ownership comes from authentication.

We do not need:

GET /api/v1/customers/{customerId}/orders

for the Customer-facing API because the caller should not control arbitrary Customer ownership scope.


Customer and Admin Access Can Share Domain Resources

An Admin may later be allowed to view a broader Order set than a Customer.

That does not require two different Order Domain models.

Security and application queries determine the permitted scope.

The underlying Order remains the same business concept.


Domain Error → HTTP Mapping

Our current canonical mapping includes:

invalid request
→ 400

authentication missing/invalid
→ 401

authenticated caller lacks authority
→ 403

Product not found
→ 404

Order missing or not owned
→ 404

Product not orderable
→ 409

insufficient Inventory
→ 409

Order not cancellable
→ 409

Order not payable
→ 409

unexpected server failure
→ 500

Handlers should not invent different semantics independently.


HTTP Semantics Should Match UseCase Semantics

Our internal operation:

CreateOrderUseCase

maps naturally to:

POST /api/v1/orders

Internal:

GetOrderHistoryUseCase

maps naturally to:

GET /api/v1/orders

Internal:

CancelOrderUseCase

maps naturally to:

POST /api/v1/orders/{orderId}/cancel

Internal:

PayOrderUseCase

maps naturally to:

POST /api/v1/orders/{orderId}/pay

This alignment makes the system easier to understand from HTTP all the way down to the Domain.


A Useful Endpoint Design Sequence

When designing an API endpoint, do not begin with:

Which Spring annotation should I use?

Instead ask:

1. What business operation or resource is involved?

2. Is it a read or a mutation?

3. What does the client control?

4. What remains server-controlled?

5. Is the operation naturally idempotent?

6. What URI best represents the resource or operation?

7. What successful outcome should the client observe?

8. What expected failures exist?

9. Which HTTP semantics represent those outcomes?

Only then should we implement the Handler.


Spring Annotation Comes Last

After the contract is understood, we choose implementation syntax such as:

@GetMapping(...)

or:

@PostMapping(...)

The annotation does not design the API.

It implements the API design.


Common Mistake — Every Endpoint Uses POST

If every endpoint is POST, we throw away useful protocol semantics.


Common Mistake — Mutation Through GET

Never use safe-read semantics for Order cancellation, Payment, Product changes, or Inventory mutation.


Common Mistake — Client Controls Server-Owned State

The client must not control:

Order status

Order total

Customer ownership

purchase-time price

createdAt

OrderId

for Order creation.


Common Mistake — Every Failure Returns 500

Expected business rejection is not an internal server crash.

Map expected failures deliberately.


Common Mistake — Every Success Returns 200

Creation, successful reads, and no-content operations have different semantics.

Use HTTP deliberately.


Common Mistake — Endpoint Names Mirror Java Methods

Avoid:

/getAllProducts

/createOrderUseCase

/setOrderStatus

Use resource and business language.


Common Mistake — DELETE for Deactivation Without Thinking

Product deactivation is not physical deletion.

Our API reflects the actual Product operation.


Common Mistake — Assuming POST Retry Is Safe

A repeated Order-creation POST can create duplicate Orders unless an explicit idempotency mechanism exists.


Common Mistake — Assuming Domain Validation Solves Request Retry

Domain invariants can protect one Order's validity.

They do not automatically prevent:

duplicate Order creation

double Payment-provider execution

Retry safety is a broader system concern.


Common Mistake — Designing APIs From Database Tables

Public API behaviour begins from:

business resources

use cases

client intent

not from automatically exposing CRUD for every table.


HTTP Review Checklist

Before approving an endpoint, ask:

Does the method match the operation semantics?

Is GET free from requested business mutation?

Is the URI resource-oriented and consistent?

Does the client control only legitimate input?

Are server-owned values generated/derived server-side?

Is retry behaviour understood?

Is the operation naturally idempotent?

Does implementation actually preserve that property?

Does success use an appropriate status?

Are expected failures distinguishable?

Could the response leak another Customer's
resource existence?

Do HTTP concerns stay at the Handler boundary?

How HTTP Fits Our Architecture

The complete direction is:

HTTP Method + URI + Headers + JSON
        ↓
Handler
        ↓
UseCase
        ↓
Domain
        ↓
Repository / External Service

Then:

Application Result / Failure
        ↓
Handler / Error Mapping
        ↓
HTTP Status + JSON

HTTP is our external contract.

It does not replace the application or Domain architecture.


Our API Design Principles

For this backend, we will follow these principles:

Use resource-oriented URIs.

Use HTTP methods according
to their intended semantics.

Never request business mutation through GET.

Keep authenticated ownership server-controlled.

Keep authoritative prices server-controlled.

Keep totals server-controlled.

Keep lifecycle state transitions server-controlled.

Use explicit business commands where
generic field mutation would weaken meaning.

Use meaningful success status codes.

Use consistent HTTP error mappings.

Keep request/response DTOs separate
from Domain behaviour.

Treat retry and idempotency
as real distributed-system concerns.

Prefer conventional predictable APIs
over clever endpoint designs.

These principles guide the rest of Module 5.


Engineering Principle

The core principle is:

HTTP is not merely a transport pipe; its methods, resource semantics, status codes, and retry behaviour are part of the application's public behavioural contract.

Another:

Clients express intent through the API while the backend remains authoritative over business identity, pricing, totals, ownership, and valid state transitions.

And:

Design the operation first, choose the HTTP representation second, and write the Spring annotation last.


Summary

In this lesson, we established that:

  • HTTP is an external application contract, not merely a set of Spring annotations.
  • Requests use methods, URIs, headers, and optional bodies to communicate intent.
  • The Handler owns the HTTP/application boundary.
  • Domain objects remain transport-independent.
  • GET represents safe read behaviour and must not request business mutation.
  • Safe methods and idempotent operations are different concepts.
  • Idempotency describes the intended final state/effect of repeated execution.
  • Idempotency does not require byte-for-byte identical responses.
  • Network timeouts may leave operation outcome ambiguous.
  • POST /api/v1/orders is appropriate for creating a new Order.
  • Repeated Order-creation POSTs may create multiple Orders without an explicit idempotency contract.
  • PUT /api/v1/inventory/{productId} fits our exact-quantity replacement semantics and is naturally idempotent.
  • PATCH /api/v1/products/{productId} represents our partial Product update contract.
  • PATCH idempotency depends on the actual patch semantics.
  • Product deactivation should not be represented as physical deletion.
  • POST /api/v1/products/{productId}/deactivate matches our explicit Domain operation.
  • Resource-oriented URIs are preferable to Java/RPC-style endpoint names.
  • UUIDs are strings at the HTTP boundary and are parsed into typed Domain IDs.
  • Query parameters are suitable for pagination/filtering/sorting, not hidden mutation.
  • Authorization metadata is handled by HTTP/security infrastructure and becomes application-friendly identity.
  • 200, 201, and 204 describe different kinds of success.
  • POST /api/v1/orders returns 201 Created on successful creation.
  • 400 represents malformed or invalid input according to our API contract.
  • 401 represents authentication not being established.
  • 403 represents an authenticated caller lacking permission for an operation.
  • 404 represents missing Products/Orders where appropriate.
  • Missing and non-owned Customer Orders both produce 404 / ORDER_NOT_FOUND.
  • 409 represents our defined business-state conflicts such as insufficient Inventory and invalid Order transitions.
  • Expected business failures do not become 500 merely because they are exceptions internally.
  • Unexpected server/infrastructure failures map to 500 / INTERNAL_ERROR.
  • API errors use structured Problem Details rather than arbitrary strings or internal exception text.
  • The Problem Details base remains https://api.tarkev.com/problems/....
  • HTTP response status and structured application error code work together.
  • Request/response DTOs remain separate from Domain objects.
  • Clients cannot control Customer ownership, Order status, authoritative price, purchase-time price, total, ID, or creation time.
  • Cancellation is a business command rather than generic Order-status assignment.
  • Payment is a business command rather than allowing the client to set PAID.
  • POST /api/v1/orders/{orderId}/cancel matches CancelOrderUseCase.
  • POST /api/v1/orders/{orderId}/pay matches PayOrderUseCase.
  • Order lifecycle idempotency and external Payment idempotency are different concerns.
  • HTTP method conventions do not automatically make an implementation safe or idempotent.
  • HTTP requests do not rely on hidden per-customer state inside singleton Spring Beans.
  • The HTTP request boundary and database transaction boundary are distinct.
  • A client timeout does not prove the server-side operation failed.
  • The public API should remain stable across internal refactoring when product behaviour does not change.
  • API design should begin from business resources and UseCases, not Repository methods or Java class names.
  • Conventional and consistent APIs are easier to integrate with than clever but inconsistent designs.
  • HTTP semantics structure the contract but do not replace documentation of business rules.
  • Spring annotations are implementation details chosen after the HTTP contract has been designed.

Next lesson:

Designing Resources and Endpoints

There, we will design the concrete API surface for Products, Inventory, and Orders, including our already established Product-management endpoints, exact Inventory replacement, Customer-owned Order history/detail, Order creation, explicit cancellation/payment commands, and the decision to expose no local Customer resource solely for ordering.