Building REST APIs

Designing Resources and Endpoints

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

আগের lesson-এ আমরা HTTP semantics দেখেছি।

এখন সেই foundation ব্যবহার করে আমাদের application-এর actual API design করতে হবে।

আমাদের main capabilities:

Product
Inventory
Order
Payment

Customer identity-ও important, কিন্তু একটি critical distinction আছে:

এই backend Customer account lifecycle own করে না।

তাই API design করার সময় আমরা blindly:

/products
/customers
/orders
/payments

এই চারটি CRUD resource বানাব না।

প্রথমে দেখব application আসলে কোন state এবং behaviour own করে।

এই lesson-এর goal:

Business capabilities থেকে clear, consistent HTTP resources এবং endpoints design করা—without exposing internal implementation or inventing a Customer subsystem.


Start From Capabilities, Not Tables

API design করার common mistake:

products table
→ /products

inventory table
→ /inventory

orders table
→ /orders

order_items table
→ /order-items

Database table থাকলেই public resource লাগবে—এমন কোনো rule নেই।

For example:

OrderItem

database-এ আলাদা rows হিসেবে persist হতে পারে।

কিন্তু current product behaviour-এ customer independently:

create OrderItem

update OrderItem

delete OrderItem

করে না।

OrderItem belongs to Order।

So:

/order-items

top-level CRUD API প্রয়োজন নেই।


Start From User and Admin Operations

Our current confirmed operations:

Customer

browse available Products

create an Order

view own Orders

view own specific Order

cancel own eligible Order

pay own eligible Order

Admin

create Product

view Product information

update Product

deactivate Product

view Inventory

set available Inventory quantity

view Orders

These operations should drive our API।


Initial Resource Map

A reasonable starting point:

/products

/inventory

/orders

Payment is primarily an operation on an existing Order in v1।

We do not currently expose Payment as a fully independent CRUD resource।

Customer is represented through authentication rather than a locally managed resource।


Customer Endpoints: What Happened to /customers?

An earlier generic milestone included:

POST /customers

GET /customers/{id}

But after requirements and architecture review, we made a stronger decision:

Customer authentication and identity lifecycle
are externally owned.

Our application does not own:

registration

password

profile lifecycle

Customer account persistence

Therefore creating:

POST /customers

would imply responsibility we explicitly do not have।

So for v1:

No Customer CRUD API.

This is not a missing endpoint।

It is a deliberate system boundary।


Authentication Supplies Customer Identity

For customer-facing operations:

HTTP Request
    ↓
Authentication
    ↓
CustomerId
    ↓
Handler
    ↓
UseCase

For example:

POST /orders

does not need:

{
  "customerId": "C-123"
}

Customer identity comes from authenticated context।

This keeps ownership server-controlled।


Product Resources

Product is application-owned state।

So:

/products

is a natural resource।

We have two broad Product use cases:

customer browsing

admin management

They may share the same resource vocabulary while authorization and representation differ appropriately।


Browse Products

Customer needs to browse currently orderable Products।

A natural endpoint:

GET /products

The response should represent Products currently available for customer ordering according to our accepted definition:

Product active
AND
available Inventory > 0

This is a read operation।

So GET fits naturally।


What Does GET /products Mean?

It does not necessarily mean:

SELECT * FROM products

It means:

Return the Products exposed by the API contract.

For customer browsing, that contract may combine:

Product
+
Inventory

internally।

The API resource is not a direct database-table mirror।


Get One Product

Potentially:

GET /products/{productId}

can return one Product representation।

For customer-facing behaviour, we need to decide whether inactive/out-of-stock Products should be visible through this endpoint.

A reasonable v1 contract is:

customer-facing Product read
follows customer visibility rules

while admin access can retrieve management state through authorized admin endpoints or representation.

We should document this explicitly when implementing the API rather than letting repository behaviour decide it accidentally।


Create Product

Admin operation:

create Product

Natural endpoint:

POST /products

Request might conceptually contain:

{
  "name": "Mechanical Keyboard",
  "price": 100.00
}

Server determines:

Product identity

initial lifecycle state

rather than allowing arbitrary server-owned fields।

On success:

201 Created

is appropriate।


Product Creation Does Not Set Inventory Automatically Without Requirement

Important:

Creating Product does not necessarily mean:

Inventory = 100

or any invented value।

Product and Inventory are separate capabilities।

If inventory must be initialized, that behaviour should come from an explicit requirement/workflow।

Do not hide an arbitrary stock rule inside Product creation।


Update Product

Admin can update Product information।

Possible endpoint:

PATCH /products/{productId}

A partial request might later support fields such as:

{
  "name": "Mechanical Keyboard Pro",
  "price": 120.00
}

This maps well to:

product.rename(...);

product.changePrice(...);

inside the appropriate UseCase/domain flow।


Why PATCH Instead of Generic POST /updateProduct?

Because:

PATCH /products/{id}

communicates:

existing Product resource

partial modification

while:

POST /updateProduct

duplicates information already carried by HTTP semantics and URI।


Should Product active Be Directly Patchable?

We need to be careful।

If API accepts:

{
  "active": false
}

then client appears to directly control lifecycle state।

Our domain language has an explicit operation:

product.deactivate();

The current requirement explicitly mentions:

deactivate Product

not generic arbitrary activation-state replacement।

Therefore an explicit endpoint may communicate intent better।


Product Deactivation

A reasonable API:

POST /products/{productId}/deactivate

This says:

Perform the Product deactivation business operation.

It maps naturally to:

DeactivateProductHandler
    ↓
DeactivateProductUseCase
    ↓
product.deactivate()

Why Not DELETE Product?

Because our business operation is:

deactivate

not:

physically delete

Using:

DELETE /products/{id}

may create the wrong expectation that the Product ceases to exist।

Existing Order history must remain meaningful।

So an explicit deactivation operation is clearer for v1।


What About Reactivation?

Current requirements do not explicitly include reactivation।

Therefore we should not automatically create:

POST /products/{id}/activate

just because deactivation exists।

API should reflect confirmed behaviour।

If reactivation becomes a requirement later, we can add it deliberately।


Inventory as a Resource

Inventory is application-owned state associated with Product।

Conceptually:

one Inventory state per Product

Customer does not directly modify Inventory।

Admin can:

view Inventory

set available quantity

Order workflows can internally decrease or restore quantity।


Inventory Resource Shape

A natural identifier is Product identity։

For example:

GET /inventory/{productId}

could return:

{
  "productId": "P-100",
  "availableQuantity": 15
}

This reflects our domain model where Inventory is naturally associated with Product।


Why Not /products/{id}/inventory?

That is also a defensible resource shape:

GET /products/{productId}/inventory

It emphasizes that Inventory belongs to Product identity।

Either structure can work।

For this course, we should choose one consistent convention rather than teaching both throughout implementation।

A clean v1 direction is:

GET /inventory/{productId}

because Inventory is a first-class capability in our application even though its identity is Product-based।


Admin Inventory Listing

Admin may need:

GET /inventory

with pagination/filtering later।

This is a collection read।

No need to expose Inventory to normal customers as a management resource।

Customer browsing receives the availability information needed through Product APIs।


Adjusting Inventory

Our accepted v1 admin behaviour:

Admin sets current available quantity to a non-negative value.

So an endpoint could be:

PUT /inventory/{productId}

with:

{
  "availableQuantity": 20
}

Why can PUT fit here?

Because the operation means:

set this Inventory resource's available quantity
to the requested value

Repeating:

{
  "availableQuantity": 20
}

produces the same intended final state।

This is naturally idempotent।


Why Not POST /inventory/{id}/add?

That would represent a different operation:

increase quantity by N

Repeated retries could increase Inventory repeatedly।

Our current requirement is simpler:

set available quantity

So a replacement/set-style operation is easier to reason about and safer to retry।


Inventory Update Request

Conceptually:

{
  "availableQuantity": 20
}

Server/domain validates:

availableQuantity >= 0

The client does not send:

previousQuantity

as an authority unless we later design concurrency-specific semantics।


Customer Cannot Modify Inventory

Endpoints such as:

PUT /inventory/{productId}

are admin-authorized operations।

This is authorization, not a different Inventory domain model।

Security module will later enforce roles।


Orders Resource

Order is the central customer-owned resource।

Natural collection:

/orders

This supports both creation and reading depending on method।


Create Order

POST /orders

Request:

{
  "items": [
    {
      "productId": "P-100",
      "quantity": 2
    },
    {
      "productId": "P-200",
      "quantity": 1
    }
  ]
}

Authenticated context supplies:

CustomerId

Server supplies:

current Product prices

Order total

initial UNPAID status

Order ID

On success:

201 Created

What Create Order Does Internally

The endpoint represents one business operation:

CreateOrderHandler
    ↓
CreateOrderUseCase

which eventually coordinates:

validate duplicate requested Products

load Products

verify active state

load Inventory

verify quantities

capture prices

decrease Inventory

construct OrderItems

construct Order

persist atomically

The API does not expose these internal implementation steps individually।


Do Not Create Endpoints for Internal Steps

Avoid APIs such as:

POST /orders/check-products

POST /orders/check-inventory

POST /orders/calculate-total

POST /orders/save

These are implementation details of one atomic operation।

The client asks:

Create this Order.

The server owns the workflow।


Order History

Authenticated customer needs own Orders।

Natural endpoint:

GET /orders

Interpretation for a customer:

Return Orders belonging to the authenticated CustomerId.

We do not need:

GET /customers/{customerId}/orders

for the normal customer flow।

Why?

Because the customer should not control whose history is requested।


Server-Controlled Ownership Scope

Bad:

GET /orders?customerId=C-999

for normal customer access।

A caller could attempt another customer's ID।

Better:

Authentication
    ↓
CustomerId
    ↓
GetOrderHistoryUseCase

The server determines the ownership filter।


Admin Order Listing

Admin may need to view all Orders।

There are multiple API design possibilities:

GET /orders

with authorization-dependent scope,

or a distinct admin-facing route/context।

We should avoid unnecessarily duplicating domain resources if authorization can clearly define access।

For now, the important rule is:

CUSTOMER
→ own Orders

ADMIN
→ broader Order access

Exact admin routing can be finalized with Security/API implementation।


Get One Order

Natural endpoint:

GET /orders/{orderId}

For a customer, the UseCase must ensure:

Order belongs to authenticated customer

Admin may have broader permission।

The Handler does not simply call:

orderRepository.findById(...)

and return whatever exists।

Ownership is part of the application contract।


Another Customer's Order

Suppose customer requests:

GET /orders/O-999

and the Order exists but belongs to somebody else।

As discussed previously, we should deliberately decide whether to return:

403

or:

404

For customer-owned resources, 404 can be useful to avoid revealing resource existence։

We can standardize this when defining the API error contract।


Order Cancellation

Cancellation is a business transition।

We do not want the client to send:

PATCH /orders/{id}
{
  "status": "CANCELLED"
}

because that makes Order state appear directly mutable।

Instead, expose intent:

POST /orders/{orderId}/cancel

This maps naturally to:

CancelOrderHandler
    ↓
CancelOrderUseCase
    ↓
order.cancel()

Why POST for Cancellation?

Cancellation is a command that changes business state।

It is not a resource read।

It also involves more than changing one field:

verify ownership

validate lifecycle

restore Inventory

persist atomically

So an explicit command endpoint is clear and practical।


Is Cancellation Idempotent?

Business-state final effect might appear idempotent:

CANCELLED

stays cancelled।

But our accepted domain behaviour treats repeated cancellation as an invalid operation because:

Inventory restoration must not occur twice

Therefore we should not casually promise retry-idempotent cancellation semantics without implementing them deliberately।

The endpoint may return a business conflict if cancellation is attempted again।


Why This Matters

HTTP method choice alone does not decide retry safety।

Even:

POST /orders/{id}/cancel

can be safely implemented or poorly implemented।

Correct CancelOrderUseCase must ensure:

eligible Order transitions once

Inventory is restored once

transaction commits atomically

Paying an Order

Payment is another explicit business action।

Natural endpoint:

POST /orders/{orderId}/pay

This means:

Attempt payment for this Order.

It does not mean:

client declares Order as PAID

Payment Endpoint Flow

Conceptually:

PayOrderHandler
    ↓
PayOrderUseCase
    ↓
verify ownership
    ↓
verify eligible UNPAID Order
    ↓
PaymentService
    ↓
external provider

On confirmed success:

order.markPaid()

No PATCH status=PAID

Avoid:

PATCH /orders/O-1001
{
  "status": "PAID"
}

This bypasses:

Payment Service

provider result

idempotency strategy

payment eligibility

and completely misrepresents the business operation।


Is Payment a Separate /payments Resource?

It could become one later if we need to expose:

Payment attempts

payment history

provider references

payment status

as independent resources।

But current scope is simpler।

Payment is currently primarily an operation attached to an Order։

So:

POST /orders/{id}/pay

is enough for the public workflow।


Do Not Invent Payment CRUD

Avoid:

POST /payments
PUT /payments/{id}
DELETE /payments/{id}

without corresponding product behaviour।

External payment integration does not automatically imply public Payment CRUD।


Current API Surface

A reasonable v1 design direction:

Products
--------
GET   /products
GET   /products/{productId}
POST  /products
PATCH /products/{productId}
POST  /products/{productId}/deactivate

Inventory
---------
GET   /inventory
GET   /inventory/{productId}
PUT   /inventory/{productId}

Orders
------
POST  /orders
GET   /orders
GET   /orders/{orderId}
POST  /orders/{orderId}/cancel
POST  /orders/{orderId}/pay

Authorization determines which caller may use each operation।


Authorization Matrix

Conceptually:

EndpointCUSTOMERADMIN
GET /productsYesYes
GET /products/{id}YesYes
POST /productsNoYes
PATCH /products/{id}NoYes
POST /products/{id}/deactivateNoYes
GET /inventoryNoYes
GET /inventory/{productId}NoYes
PUT /inventory/{productId}NoYes
POST /ordersYesNot necessarily a normal admin operation
GET /ordersOwn ordersAll/broader access
GET /orders/{id}Own orderBroader access
POST /orders/{id}/cancelOwn eligible orderDepends on product requirement
POST /orders/{id}/payOwn eligible orderNot a normal admin operation

We should not invent admin permissions beyond confirmed behaviour।

The important confirmed roles are:

CUSTOMER
→ create Order
→ view own Orders

ADMIN
→ manage Product
→ manage Inventory
→ view all Orders

Cancellation/payment customer behaviour is also part of our accepted scope।


Do Not Encode Roles in URIs by Default

We could create:

/admin/products
/customer/orders

But role prefixes are not always necessary।

Resources remain:

/products
/orders

and authorization can determine access।

This keeps API resource vocabulary independent from organizational role names।


When Separate Admin Routes Might Be Useful

Sometimes admin representation or workflows differ enough that:

/admin/...

becomes valuable।

But we do not need to decide that prematurely।

Start with resource semantics, then separate routes only if the contracts genuinely diverge।


OrderItem Is Nested Representation, Not Top-Level Resource

Order response may contain:

{
  "id": "O-1001",
  "status": "UNPAID",
  "items": [
    {
      "productId": "P-100",
      "quantity": 2,
      "unitPrice": 20.00
    }
  ],
  "total": 40.00
}

This does not require:

GET /order-items/{id}

OrderItems are naturally represented inside Order।


No Endpoint to Change Order Items

Current requirements do not support:

POST /orders/{id}/items

PATCH /orders/{id}/items/{itemId}

DELETE /orders/{id}/items/{itemId}

because Order is not a Cart।

Successful Order composition is immutable in v1।


No Endpoint for Setting Order Total

Obviously avoid:

PUT /orders/{id}/total

Total is derived from OrderItems।

The server owns it।


No Endpoint for Setting Customer Ownership

Avoid:

PATCH /orders/{id}
{
  "customerId": "C-200"
}

Order ownership is established at creation from authenticated identity and does not have a current reassignment workflow।


Resource Creation and IDs

For:

POST /products

and:

POST /orders

the server may generate IDs։

Exact ID strategy remains deferred।

Clients should not need to know whether we internally use:

UUID

database sequence

another identifier

The API simply returns the resource ID according to its contract।


Location Header

A successful creation response may include:

Location: /orders/O-1001

alongside:

201 Created

This can be useful because it identifies the newly created resource։

Whether we use it consistently will be decided in Handler/API implementation, but it is a natural HTTP pattern।


Request Body Should Represent Intent

Create Product:

{
  "name": "Keyboard",
  "price": 100.00
}

Create Order:

{
  "items": [
    {
      "productId": "P-100",
      "quantity": 2
    }
  ]
}

Set Inventory:

{
  "availableQuantity": 10
}

Notice each request contains only values the caller legitimately controls।


Avoid Generic Update DTOs

A tempting pattern:

{
  "id": "...",
  "type": "...",
  "status": "...",
  "active": "...",
  "operation": "..."
}

for many endpoints।

This usually produces ambiguous contracts।

Prefer endpoint-specific request models where operations have different intent।


Endpoint-Specific DTOs

Examples:

CreateProductRequest

UpdateProductRequest

SetInventoryRequest

CreateOrderRequest

Business command endpoints such as:

cancel

pay

deactivate

may need no request body at all if all required intent is already represented by:

authenticated caller
+
resource ID

and no additional input is required।


Do Not Send Empty JSON Just Because POST Has a Body

For:

POST /orders/{id}/cancel

if no additional client input is required, we do not need:

{}

merely because it is POST।

A request body should exist when it carries meaningful input।


Payment May Need Input Later

Depending on Payment Provider contract, payment operation may eventually require:

payment method token

provider-specific payment reference

other approved payment input

We should not invent those fields now।

The endpoint shape can remain:

POST /orders/{id}/pay

while the eventual request DTO is defined from the actual provider/product requirement।


Product Browse Response

Customer-facing Product response might conceptually include:

{
  "id": "P-100",
  "name": "Keyboard",
  "price": 100.00,
  "availableQuantity": 5
}

Should availableQuantity be exposed?

Maybe।

The requirement says customers should browse useful Product information and availability।

But exact visibility of stock count is a product decision।

We should not automatically expose internal Inventory quantity if the product only needs:

in stock / out of stock

So response design must follow confirmed product needs।


Do Not Leak More Data Than Needed

This principle applies broadly।

Customer Product response does not necessarily need:

internal audit fields

database version

admin-only state

raw Inventory metadata

Order response does not need:

provider secrets

internal persistence IDs

security details

DTOs define the public contract intentionally।


Collection Endpoints Need Bounds

Both:

GET /products

and:

GET /orders

must be bounded/paginated according to our requirements।

We should not design:

GET all rows forever

even if the first development database contains ten records।

Pagination details come in a dedicated lesson।


Filtering

Potential Product/Order filtering may later use query parameters such as:

GET /products?... 
GET /orders?...

But we should only add filters with real product requirements।

Do not create twenty optional query parameters upfront।


API Design and UseCases

A good endpoint should generally map to a meaningful Handler/UseCase boundary।

Examples:

POST /orders
→ CreateOrderHandler
→ CreateOrderUseCase
POST /orders/{id}/cancel
→ CancelOrderHandler
→ CancelOrderUseCase
POST /orders/{id}/pay
→ PayOrderHandler
→ PayOrderUseCase
PUT /inventory/{productId}
→ SetInventoryHandler
→ SetInventoryUseCase

Do Not Force One Handler Per HTTP Method Mechanically

We can still group closely related HTTP mappings in a Spring controller implementation if that keeps code simpler।

Our architecture terminology focuses on responsibilities:

Handler
→ transport/application entry boundary

It does not require one Java class per endpoint as a universal rule।

We will implement pragmatically while preserving:

Handler → UseCase → Repository

for meaningful workflows।


Reads May Be Simpler

For read-only queries, a UseCase may remain small:

GetOrderHistoryUseCase
→ query Repository
→ return result

That's fine।

Do not remove the application boundary just because there is little mutation logic if ownership/filtering/business query semantics still matter։


Avoid CRUD-Generated API Thinking

If we generated CRUD from tables, we might end up with:

POST /order-items
DELETE /orders
PATCH /inventory-row
POST /customers

which conflicts with actual business behaviour।

Our API instead reflects:

what customers/admins are allowed to do

That is much closer to product design।


Endpoint Naming Should Use Nouns for Resources

Prefer:

/products

/orders

/inventory

Business action suffixes are useful when the operation does not map cleanly to direct resource replacement:

/orders/{id}/cancel

/orders/{id}/pay

/products/{id}/deactivate

Avoid unnecessary verbs in standard resource operations:

/getProducts

/createProduct

/getOrder

because HTTP methods already communicate those actions।


Avoid Deep Nesting

Potentially ugly:

/customers/{customerId}/orders/{orderId}/items/{itemId}

We do not need this hierarchy।

Customer scope comes from authentication।

Order Item is not independently managed।

Simple:

/orders/{orderId}

is enough, with ownership enforced server-side।


URI Does Not Need to Encode Every Relationship

The database may contain:

Order → CustomerId

OrderItem → ProductId

but URI hierarchy does not need to reproduce all relations։

REST resources are an external API model, not a visual representation of foreign keys।


Command Endpoint Responses

For:

POST /orders/{id}/cancel

possible successful response approaches include:

200 + updated Order representation

or:

204 No Content

Both can be valid।

Returning the updated Order can be useful because client immediately sees:

status = CANCELLED

For this course, we can favor returning the resulting resource representation for important state transitions unless there is a reason not to.

Exact response contract will be handled when implementing handlers।


Payment Response

Similarly, successful:

POST /orders/{id}/pay

might return updated Order representation showing:

PAID

or a dedicated result if payment workflow requires additional public information।

Do not return raw provider response DTOs।

Provider protocol remains behind PaymentService


External Provider Types Must Not Leak

Bad payment response:

{
  "providerInternalStatus": "CAPTURED_X7",
  "rawGatewayCode": "...",
  "debugResponse": "..."
}

unless those are explicitly part of our API product contract।

External provider details should be mapped to application-facing concepts।


Error Semantics Are Part of Endpoint Design

For each endpoint, ask expected failures。

Example:

POST /orders

Possible business failures:

Product not found

Product inactive

duplicate Product

insufficient Inventory

invalid quantity

Example:

POST /orders/{id}/cancel

Possible:

Order not found/not accessible

Order already paid

Order already cancelled

Example:

POST /orders/{id}/pay

Possible:

Order not found/not accessible

Order already paid

Order cancelled

Payment Service failure

These must eventually map consistently to our API error model।


Do Not Design Only the Happy Path

An endpoint is not complete merely because:

POST /orders
→ 201

works।

Professional API design also defines:

what client errors mean

what business conflicts mean

what authentication failures mean

what provider/infrastructure failures mean

We will refine this throughout the module।


Endpoint Review: Products

Our current direction:

GET /products
GET /products/{productId}
POST /products
PATCH /products/{productId}
POST /products/{productId}/deactivate

Why no:

DELETE /products/{id}

Because physical deletion is not the business operation।

Why no:

POST /products/{id}/activate

Because reactivation is not yet a requirement।


Endpoint Review: Inventory

Current direction:

GET /inventory
GET /inventory/{productId}
PUT /inventory/{productId}

The update is:

set available quantity

not:

increment by arbitrary delta

for the admin API।

Order creation/cancellation modifies Inventory internally through UseCases, not through public Inventory HTTP calls।


Endpoint Review: Orders

Current direction:

POST /orders
GET /orders
GET /orders/{orderId}
POST /orders/{orderId}/cancel
POST /orders/{orderId}/pay

No:

PATCH status

edit items

set total

change owner

because those would violate our domain model।


Endpoint Review: Customers

Current direction:

No Customer CRUD endpoints.

Customer identity comes from authentication।

This is a deliberate correction from the earlier generic API milestone।

Our accepted architecture is now the source of truth।


Endpoint Review: Order Items

Current direction:

No independent OrderItem CRUD.

OrderItems are contained in Order request/response representations and managed through Order creation lifecycle।


Endpoint Review: Payments

Current direction:

POST /orders/{orderId}/pay

No general public Payment CRUD until actual payment-domain requirements justify it।


API Surface Should Stay Small

A small coherent API is better than exposing every internal capability mechanically।

Our v1 surface supports the required product behaviour without introducing:

Customer subsystem

OrderItem CRUD

Payment CRUD

Product deletion

Inventory adjustment history API

refund APIs

reservation APIs

because those do not belong to current scope।


A Useful Endpoint Design Checklist

For every proposed endpoint ask:

Which business capability owns this operation?

Is this resource actually owned by our system?

Is it a read or a mutation?

Does the HTTP method communicate that correctly?

What input does the client legitimately control?

Which values must remain server-controlled?

Does this expose a domain operation
or merely a database field mutation?

Is this resource independently managed?

Is authenticated identity being trusted
instead of request-supplied ownership?

What is the retry/idempotency behaviour?

What are the expected failures?

Does this endpoint create a feature
that is not in the requirements?

Common Mistake 1 — CRUD for Every Domain Class

OrderItem does not need public CRUD just because it exists as a domain/persistence type।


Common Mistake 2 — /customers Because Commerce Has Customers

Our backend references externally owned customer identity; it does not own Customer account lifecycle।


Common Mistake 3 — PATCH status=PAID

Payment is a workflow, not arbitrary state mutation।


Common Mistake 4 — DELETE /products

The confirmed operation is Product deactivation, not physical deletion।


Common Mistake 5 — Client-Supplied customerId

Authenticated identity controls Order ownership।


Common Mistake 6 — Client-Supplied Price or Total

Backend uses current server-side Product price and derives total।


Common Mistake 7 — Public Inventory Endpoint Used by Order Creation

Internal UseCases coordinate Inventory directly through Repository/domain logic।

We do not make our own application call its public HTTP API internally।


Common Mistake 8 — Expose Provider Payment DTOs

Third-party protocol belongs behind PaymentService


Common Mistake 9 — Unlimited Collection Reads

Products and Orders must remain bounded/paginated।


Common Mistake 10 — Build Endpoints for Imagined Future Features

No refund, Cart editing, reservation, Product reactivation, or Customer profile API until requirements exist।


Our V1 API Direction

The current working design is:

PRODUCTS

GET   /products
GET   /products/{productId}
POST  /products
PATCH /products/{productId}
POST  /products/{productId}/deactivate
INVENTORY

GET   /inventory
GET   /inventory/{productId}
PUT   /inventory/{productId}
ORDERS

POST  /orders
GET   /orders
GET   /orders/{orderId}
POST  /orders/{orderId}/cancel
POST  /orders/{orderId}/pay

And intentionally:

NO /customers CRUD

NO /order-items CRUD

NO generic /payments CRUD

for current v1।


Engineering Principle

The core principle:

Expose the operations and resources the application actually owns—not every noun, database table, or Java class inside the implementation.

Another:

Use HTTP resources for state clients may legitimately interact with, and explicit command endpoints for meaningful business transitions such as cancellation, payment, and deactivation.

And:

Authenticated identity, authoritative prices, totals, and lifecycle transitions remain under server control.


Summary

In this lesson, we learned that:

  • API resources should come from owned product capabilities, not database tables.
  • Product, Inventory, and Order are meaningful API resources in the current system.
  • Customer account lifecycle is externally owned, so the Order Management Backend does not expose Customer CRUD solely for ordering.
  • Authenticated CustomerId replaces request-controlled customer ownership.
  • OrderItem is an owned part of Order and does not need independent CRUD endpoints.
  • Product browsing uses GET /products.
  • Product creation naturally uses POST /products.
  • Product partial updates can use PATCH /products/{id}.
  • Product deactivation is better expressed as an explicit business operation than physical deletion.
  • Inventory can be addressed through Product identity.
  • Admin Inventory quantity setting can use an idempotent set-style operation such as PUT /inventory/{productId}.
  • Order creation uses POST /orders.
  • GET /orders for customers should be scoped by authenticated identity rather than a client-supplied customerId.
  • Order cancellation is an explicit lifecycle operation, not generic status mutation.
  • Payment is an explicit Order operation and must not be represented as status=PAID.
  • Internal Order creation steps should not become separate public endpoints.
  • Order item composition, ownership, price, total, and lifecycle state remain server-controlled.
  • Collection endpoints must eventually be bounded/paginated.
  • Admin and customer authorization can operate over the same domain resources without automatically encoding role names in every URI.
  • External payment-provider models must remain behind the PaymentService boundary.
  • The v1 API should remain deliberately small and should not expose speculative future features.

Next lesson:

Controllers in Spring Boot

There we will map this API design into Spring Boot HTTP handlers using @RestController, request mappings, path variables, request bodies, and constructor-injected UseCases—while keeping Controllers/Handlers thin and preserving our Handler → UseCase → Repository architecture.