Building REST APIs

Request and Response Models

ReadingPreview

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

আমরা এখন পর্যন্ত API endpoint এবং Spring Controller structure define করেছি।

For example:

POST /products
PATCH /products/{productId}

PUT /inventory/{productId}

POST /orders
GET /orders
GET /orders/{orderId}

এখন প্রতিটি endpoint-এর আরেকটি গুরুত্বপূর্ণ অংশ design করতে হবে:

Request Model

Response Model

Spring MVC-তে এগুলো সাধারণত DTO হিসেবে represent করা হবে।

For example:

CreateOrderRequest
OrderResponse

একটি ভালো API design শুধু endpoint path এবং HTTP method নিয়ে নয়।

এটাও define করতে হয়:

Client কোন data পাঠাতে পারবে?

এবং:

Server কোন data public contract হিসেবে expose করবে?

এই lesson-এর goal:

Transport DTOs-কে domain model থেকে আলাদা রেখে explicit, safe, এবং operation-specific API contracts তৈরি করা।


DTOs Exist at a Boundary

DTO:

Data Transfer Object

এর responsibility হলো:

data এক boundary থেকে আরেক boundary-তে বহন করা

HTTP API-তে:

JSON
    ↓
Request DTO
    ↓
Handler

এবং:

Handler
    ↓
Response DTO
    ↓
JSON

DTO business Entity নয়।

DTO persistence Entity-ও নয়।


Request Model and Response Model Are Different Responsibilities

একটি common mistake:

public record ProductDto(
        String id,
        String name,
        BigDecimal price,
        boolean active
) {
}

তারপর একই class use করা হয়:

Product create request

Product update request

Product response

admin response

customer response

এটি simple মনে হলেও দ্রুত ambiguous হয়ে যায়।

কারণ প্রতিটি operation-এর authority আলাদা।


Create Product Does Not Need Product ID

Consider:

POST /products

Client create করতে চায়:

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

Client should not send:

{
  "id": "P-100",
  "name": "Mechanical Keyboard",
  "price": 100.00,
  "active": false
}

if:

Product ID

and initial state server-controlled।

Therefore:

CreateProductRequest

should not simply mirror Product


A Better CreateProductRequest

Conceptually:

public record CreateProductRequest(
        String name,
        BigDecimal price
) {
}

This expresses exactly what client controls।

Nothing more।


ProductResponse

Server may return:

{
  "id": "P-100",
  "name": "Mechanical Keyboard",
  "price": 100.00,
  "active": true
}

So response model can be:

public record ProductResponse(
        String id,
        String name,
        BigDecimal price,
        boolean active
) {
}

Request and response look related, but they are intentionally different।


Client Input and Server Output Are Not Symmetric

This is one of the most important API design principles।

A client may be allowed to send:

name
price

while server returns:

id
name
price
active

That is perfectly normal।

API design should not force:

request fields == response fields

Why Reusing One DTO Can Be Dangerous

Suppose we use:

ProductDto

for create request and response।

Then ProductDto contains:

id

active

because response needs them।

Now create endpoint automatically appears to accept those fields too।

Even if implementation ignores them, contract becomes confusing।

Worse, someone later may accidentally use them।

Operation-specific DTOs remove this ambiguity।


UpdateProductRequest

Our Product update endpoint:

PATCH /products/{productId}

means partial modification।

So the request may support:

name

price

as optional changes।

Conceptually:

public record UpdateProductRequest(
        String name,
        BigDecimal price
) {
}

But now we encounter an important PATCH problem:

How do we distinguish "field not supplied" from "field supplied as null"?

This matters for partial updates।


PATCH Semantics Need Deliberate Modeling

Suppose JSON:

{
  "price": 120.00
}

means:

change price

leave name unchanged

But:

{
  "name": null
}

could mean either:

clear the name

or:

invalid request

depending on API contract।

For our Product domain:

Product name cannot be blank/null

so clearing name is not a valid operation।

Still, we need to know whether the field was omitted or supplied invalidly।


Don't Solve PATCH Accidentally

For a simple course application, there are several options:

nullable request fields

Optional fields

JSON-aware patch types

separate operation endpoints

Each has trade-offs।

We should not introduce a complex PATCH framework prematurely।

For our current Product update needs, a pragmatic DTO can use nullable fields where:

null
→ no update requested

provided the contract explicitly says clients cannot set Product name/price to null।

Then UseCase can process only supplied values।


But Nullable Values Need Care

Conceptually:

public record UpdateProductRequest(
        String name,
        BigDecimal price
) {
}

UseCase/Handler may interpret:

if (request.name() != null) {
    product.rename(request.name());
}

if (request.price() != null) {
    product.changePrice(request.price());
}

This is acceptable for a small API where null itself is never a legitimate domain value।

But don't blindly apply this approach when null is a valid explicit state।


Request DTO Is Not the Domain Object

Even though:

CreateProductRequest

contains:

name
price

and Product contains:

name
price

they have different responsibilities।

Request DTO:

represents untrusted client input

Product:

represents valid business state

That distinction remains critical।


Transport Input Can Be Invalid

Spring may successfully deserialize:

{
  "name": "",
  "price": -10
}

into:

new CreateProductRequest(
        "",
        new BigDecimal("-10")
);

The fact that DTO exists does not mean Product can be validly created।

Later validation plus domain invariants protect the transition from:

untrusted transport data

to:

trusted domain state

Inventory Request Model

Current admin operation:

PUT /inventory/{productId}

Request:

{
  "availableQuantity": 20
}

The Product identity already exists in the URI।

So request model can be:

public record SetInventoryRequest(
        int availableQuantity
) {
}

No need to duplicate:

productId

inside the body।


Avoid Duplicated Identity

Bad:

PUT /inventory/P-100

with:

{
  "productId": "P-200",
  "availableQuantity": 20
}

Now we have conflicting identity values।

Better:

URI
→ target identity

body
→ requested state/input

So:

SetInventoryRequest

only contains quantity।


InventoryResponse

Conceptually:

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

Response:

public record InventoryResponse(
        String productId,
        int availableQuantity
) {
}

Response can include identity because client needs to understand which Inventory state was returned।


CreateOrderRequest

Order creation has a much more important trust boundary।

Client should provide:

Product ID

quantity

for each requested line।

Client should not provide:

CustomerId

OrderId

OrderStatus

unitPrice

Order total

because all of these are server-controlled।


A Good CreateOrderRequest

Conceptually:

public record CreateOrderRequest(
        List<CreateOrderItemRequest> items
) {
}

and:

public record CreateOrderItemRequest(
        String productId,
        int quantity
) {
}

This represents exactly the customer's ordering intent।


Bad CreateOrderRequest

Avoid:

public record CreateOrderRequest(
        String customerId,
        String status,
        BigDecimal total,
        List<CreateOrderItemRequest> items
) {
}

and:

public record CreateOrderItemRequest(
        String productId,
        int quantity,
        BigDecimal unitPrice
) {
}

Why?

Because now client appears authoritative over:

ownership

lifecycle

pricing

total

which contradicts our domain design।


Server Builds Trusted Order State

Correct flow:

CreateOrderRequest
    ↓
ProductId + quantity
    ↓
CreateOrderUseCase

Then backend obtains:

CustomerId
→ authenticated context

Product price
→ ProductRepository / Product state

Order status
→ UNPAID

total
→ derived from OrderItems

Then:

Order

is created from trusted values।


Request Model Should Reflect Intent, Not Result

Client says:

I want 2 units of Product P-100.

Client does not say:

The price was 20.

The total is 40.

This Order is UNPAID.

Those are outcomes/state determined by the server।

This distinction should be visible directly in the DTO design।


OrderResponse

A customer needs a useful representation of an Order।

Conceptually:

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

A corresponding response model could be:

public record OrderResponse(
        String id,
        String status,
        List<OrderItemResponse> items,
        BigDecimal total
) {
}

and:

public record OrderItemResponse(
        String productId,
        int quantity,
        BigDecimal unitPrice,
        BigDecimal total
) {
}

Why Response Can Include Unit Price

Unlike the create request, response can expose:

unitPrice

because this is the server-confirmed purchase-time price stored in the OrderItem।

This is historical Order information।

The client may read it।

The client did not choose it।


Derived Values Can Be Returned

Response may include:

OrderItem total

Order total

even though those are derived values।

That's fine।

A response DTO is allowed to contain convenient client-facing representations of server-owned state।

The important part is:

client cannot authoritatively send them back

as business state mutation।


Request and Response Models Need Not Share Types

Could we use:

OrderItemDto

for both incoming and outgoing items?

Incoming needs:

productId
quantity

Outgoing may need:

productId
quantity
unitPrice
total

These are different contracts।

So separate:

CreateOrderItemRequest

OrderItemResponse

is clearer।


Avoid Universal OrderDto

A universal:

public record OrderDto(
        String id,
        String customerId,
        String status,
        List<OrderItemDto> items,
        BigDecimal total,
        ...
) {
}

may eventually be reused everywhere:

create

update

list

detail

payment

admin

Then every endpoint exposes fields it doesn't need।

Prefer operation-specific or audience-specific models where contracts differ materially।


But Don't Create DTO Explosion Either

We also don't need:

CreateOrderResponse

GetOrderResponse

CancelOrderResponse

PayOrderResponse

if all four endpoints intentionally return the same public Order representation।

A shared:

OrderResponse

is appropriate when the output contract genuinely matches।

The principle is:

Reuse because semantics are the same, not because the names are similar.


List Response vs Detail Response

Suppose later Order history only needs:

id

status

total

createdAt

while Order detail needs full:

items

Then having:

OrderSummaryResponse

OrderResponse

may be better than returning large detailed Order objects in every list result।

But we should introduce that distinction only when the API requirement benefits from it।


Customer Response vs Admin Response

Similarly, admin Product view may eventually require fields customers should not see।

That could justify:

ProductResponse

AdminProductResponse

or separate representation strategies।

But don't create both before a real contract difference exists।


DTO Design Should Follow Audience and Operation

Useful questions:

Who receives this response?

What do they need?

What are they allowed to know?

What data is stable public contract?

Which fields are internal implementation details?

This is more useful than:

Which fields are available on the Entity?


Do Not Return Customer Ownership Carelessly

Should customer OrderResponse contain:

customerId

?

For the customer viewing their own Order, this may add little value because ownership is already implied by authentication।

For admin views, customer identity may be relevant।

This demonstrates why response design should follow API needs rather than simply serialize all Entity fields।


Avoid Exposing Internal Persistence Fields

Later JPA models may contain:

database IDs

version columns

technical timestamps

foreign-key mapping details

These should not automatically become API fields।

Example:

@Version
private long version;

may be required for persistence concurrency.

That does not mean customer response should expose:

{
  "version": 7
}

unless the API contract intentionally uses it।


Do Not Return JPA Entity Directly

Suppose later:

@Entity
public class OrderEntity {
    ...
}

Controller returning it directly:

return orderEntity;

can accidentally expose:

persistence relationships

lazy-loaded collections

internal columns

ORM implementation details

It also couples database mapping changes to API changes।

Use response DTOs।


Do Not Use Domain Entity as JSON Contract by Default

Likewise:

return order;

from Controller can couple public API to internal domain representation।

Suppose domain later changes:

OrderStatus field structure

typed IDs

money representation

internal methods/state

We should not automatically break API clients।

DTO mapping creates a deliberate boundary।


Domain Types Can Still Appear Internally

Inside application code:

OrderId

CustomerId

ProductId

are useful strongly typed concepts।

Transport may represent them as:

String

in JSON।

Handler mapping translates between those forms।

This keeps JSON simple without weakening internal type safety।


Why Not Expose ProductId Record Directly?

Suppose:

public record ProductId(
        String value
) {
}

If Jackson serializes it directly, JSON might become:

{
  "productId": {
    "value": "P-100"
  }
}

when the public contract really wants:

{
  "productId": "P-100"
}

We could customize serialization, but response mapping may be much simpler।

Transport shape should be designed intentionally।


DTO Mapping

A simple response mapping:

public record ProductResponse(
        String id,
        String name,
        BigDecimal price,
        boolean active
) {

    public static ProductResponse from(
            Product product
    ) {
        return new ProductResponse(
                product.id().value(),
                product.name(),
                product.price(),
                product.isActive()
        );
    }
}

This is clear and explicit।


OrderItemResponse Mapping

Conceptually:

public record OrderItemResponse(
        String productId,
        int quantity,
        BigDecimal unitPrice,
        BigDecimal total
) {

    public static OrderItemResponse from(
            OrderItem item
    ) {
        return new OrderItemResponse(
                item.productId().value(),
                item.quantity(),
                item.unitPrice(),
                item.total()
        );
    }
}

Again, mapping is straightforward।

No mapper framework needed।


OrderResponse Mapping

Conceptually:

public record OrderResponse(
        String id,
        String status,
        List<OrderItemResponse> items,
        BigDecimal total
) {

    public static OrderResponse from(
            Order order
    ) {
        List<OrderItemResponse> items =
                order.items()
                        .stream()
                        .map(
                                OrderItemResponse::from
                        )
                        .toList();

        return new OrderResponse(
                order.id().value(),
                order.status().name(),
                items,
                order.total()
        );
    }
}

Transport conversion remains outside the domain।


Is status().name() Good API Design?

Using:

order.status().name()

would expose values such as:

UNPAID
PAID
CANCELLED

This can be perfectly acceptable if we intentionally define those as the public API enum values।

But once published, enum values become part of the contract।

Renaming:

UNPAID

to:

AWAITING_PAYMENT

inside Java could then become an API-breaking change if mapped directly through .name()


Explicit Enum Mapping Can Protect the Contract

If we want stronger separation, we could define:

public enum OrderStatusResponse {
    UNPAID,
    PAID,
    CANCELLED
}

or map to explicit strings।

But that's additional code।

For our current small API, direct mapping may be pragmatic if the values are intentionally part of the public contract।

The key is to recognize the coupling rather than create it accidentally।


DTOs and Java record

Java records are a strong fit for many request/response DTOs because DTOs are normally:

small

data-oriented

immutable after construction

Example:

public record SetInventoryRequest(
        int availableQuantity
) {
}

We don't need setters or mutable transport objects for simple JSON contracts।


Records Do Not Replace Validation

A Java record can still contain invalid values:

new SetInventoryRequest(-5);

unless validation exists।

Immutability means the value doesn't mutate after construction।

It does not automatically mean the value is valid।


Request DTO Validation Comes Next

In the next validation lesson we will add rules such as:

CreateProductRequest
→ name required

CreateOrderRequest
→ items required

CreateOrderItemRequest
→ quantity positive

SetInventoryRequest
→ quantity non-negative

using transport validation appropriately।

But remember domain invariants remain important too।


JSON Field Naming

Java field:

availableQuantity

naturally becomes JSON:

{
  "availableQuantity": 20
}

through Jackson's default conventions।

That's simple and fine।

Avoid adding:

@JsonProperty(...)

everywhere unless we intentionally want a different public name।

Convention reduces unnecessary configuration।


API Contract Should Be Stable

Once clients depend on:

{
  "availableQuantity": 20
}

changing it later to:

{
  "stock": 20
}

can be a breaking change।

DTO field names therefore deserve deliberate naming։

They are not merely implementation details once published।


Avoid Abbreviations Without Value

Prefer:

availableQuantity

over:

qtyAvail

unless the product/domain vocabulary actually uses that abbreviation।

API clarity matters because clients may not share our internal context։


Request Models Should Avoid Internal Naming

Suppose database column is:

available_qty

That does not mean Java/JSON must use:

available_qty

or:

inventoryAvailableQtyInternal

Transport vocabulary should reflect API/domain meaning, not schema implementation।


Response Models Should Be Deliberately Bounded

A common mistake is to think:

More fields are more helpful.

But every exposed field becomes:

documentation

support burden

potential compatibility concern

potential security/privacy concern

Return what clients need।

Not everything the server knows।


Don't Leak External Provider Fields

Payment provider may eventually return:

providerTransactionId

rawStatus

gatewayCode

debugMessage

Our Order response should not automatically expose these।

If clients need a payment reference later, define an application-facing contract deliberately।

Provider DTO remains behind:

PaymentService

Request DTO Should Not Include Internal Workflow Flags

Avoid things like:

{
  "skipInventoryCheck": true,
  "forcePaid": true,
  "ignoreProductStatus": true
}

These fields expose internal business controls and can bypass invariants।

Public request DTOs should represent legitimate product operations, not implementation escape hatches։


Don't Use One Generic Request Class

Bad:

public record GenericRequest(
        Map<String, Object> data
) {
}

for stable APIs।

Then every Handler must manually interpret:

which key exists?

what type is it?

what does it mean?

Typed request models give us explicit contracts։


Avoid Generic Response Envelopes Without Need

Some APIs wrap everything:

{
  "success": true,
  "data": {
    ...
  }
}

This can be valid as an organization-wide convention।

But we should not invent a wrapper simply because it looks enterprise-like।

For this course, focus first on:

HTTP status

typed response body

consistent error model

If a response envelope is required by a broader platform standard, that is a separate architectural contract।


Response Model Is Not a View of the Database

Suppose Product and Inventory are stored separately।

Customer Product response could still conceptually combine:

{
  "id": "P-100",
  "name": "Keyboard",
  "price": 100.00,
  "available": true
}

even though no table contains exactly those fields together।

Response models are application-facing projections।

They do not need a one-to-one relationship with persistence models।


Read Models Can Be Optimized Later

For browsing, we may eventually avoid constructing full:

Product
+
Inventory

domain objects if an efficient query projection is enough।

For example repository/query layer could produce:

ProductBrowseItem

containing only fields needed for response։

That's a later persistence/query optimization।

The public ProductResponse contract can remain stable।


DTO Placement

Capability-first structure:

product/
└── handler/
    ├── ProductHandler.java
    ├── CreateProductRequest.java
    ├── UpdateProductRequest.java
    └── ProductResponse.java

Inventory:

inventory/
└── handler/
    ├── InventoryHandler.java
    ├── SetInventoryRequest.java
    └── InventoryResponse.java

Order:

order/
└── handler/
    ├── OrderHandler.java
    ├── CreateOrderRequest.java
    ├── CreateOrderItemRequest.java
    ├── OrderResponse.java
    └── OrderItemResponse.java

HTTP DTOs remain near HTTP Handlers।


Avoid a Global dto/ Package

A root package like:

dto/
├── ProductDto
├── OrderDto
├── InventoryDto
├── PaymentDto
└── ...

quickly becomes a mixed dumping ground।

Keeping DTOs near their capability makes ownership clearer।


Application Commands Belong Somewhere Else

If we introduce:

CreateOrderCommand

that is not an HTTP DTO।

It belongs near:

order/usecase/

because it represents application-level input।

Flow:

CreateOrderRequest
    ↓
Handler
    ↓
CreateOrderCommand
    ↓
CreateOrderUseCase

Do not mix:

request DTO

and:

application command

into one global DTO taxonomy।


When Request DTO Can Go Directly to UseCase

If CreateProductRequest is simple, we may do:

createProductUseCase.execute(
        request.name(),
        request.price()
);

No command required।

This avoids unnecessary mapping।

The important thing is that the UseCase does not become dependent on HTTP-specific annotations or semantics।


Do Not Pass CreateProductRequest Into Domain

Avoid:

new Product(request);

Now Product depends conceptually on HTTP transport model।

Better:

new Product(
        id,
        request.name(),
        request.price()
);

or more commonly the UseCase performs construction।

Domain should receive domain values, not request wrappers।


Handling Lists Safely

For Order request:

public record CreateOrderRequest(
        List<CreateOrderItemRequest> items
) {
}

Even though the record field is final, the incoming List itself could conceptually be mutable in plain Java usage।

When mapping into application/domain state, use deliberate copying where appropriate।

The domain Order already protects itself with:

List.copyOf(items);

Transport DTO does not need to be our final invariant boundary।


Response Collections Should Also Be Stable

A response record containing:

List<OrderItemResponse>

can receive an immutable list from:

stream().toList()

in modern Java।

This makes response construction easier to reason about।

Again, DTO immutability is useful but not a substitute for domain invariants।


Date and Time Responses

Order history will likely expose:

createdAt

when that field is introduced concretely।

Internally we may use:

Instant

API may serialize it in a standard timestamp representation։

For example conceptually:

2026-08-09T18:30:00Z

We should avoid locale-specific strings such as:

09/08/2026 18:30

as the canonical API contract unless explicitly required।

Exact timestamp contract can be finalized when createdAt is introduced।


Money Representation

Our domain currently uses:

BigDecimal

for prices and totals।

JSON numbers can represent these values naturally։

For example:

{
  "price": 100.00
}

But an API must eventually define:

currency assumption

scale/precision expectations

if those become relevant।

Current course scope does not introduce multi-currency։

Do not invent a complex money JSON object prematurely।


Should We Return double?

No reason to convert domain BigDecimal to:

double

for the response DTO।

Keep:

BigDecimal

through the transport model where practical।

This avoids introducing binary floating-point representation into monetary values।


Product Browse Response and Inventory

Should customer ProductResponse contain:

availableQuantity

?

We have not confirmed that exact stock count is part of the customer-facing product contract।

The requirement only needs customers to browse currently orderable Products with useful information։

Therefore we should not automatically expose stock quantity merely because Inventory has it।


Possible Customer Product Representation

For example:

public record ProductResponse(
        String id,
        String name,
        BigDecimal price
) {
}

could be enough if:

GET /products

already filters out unavailable Products।

Then availability is implied by inclusion in the collection।

This is a valid, smaller contract।


Admin May Need More Product State

Admin might need:

active status

even though customer browsing may not।

That creates a future decision:

same response with more fields
or
separate admin representation

We should choose based on actual endpoint contract, not force one universal Product DTO now।


API Response Should Not Lie

If GET /products returns only orderable Products, don't include:

{
  "active": true
}

merely because every returned item is necessarily active, unless clients genuinely need that field।

Redundant fields can create future confusion।


Order Response and CustomerId

For customer:

GET /orders/{id}

returning customerId may be unnecessary।

For admin Order view, it may be important।

This is another case where client audience can eventually justify different output models।

Do not expose ownership identifiers by default just because Order contains one।


Security Through DTO Design

DTOs are not the complete security system, but they reduce attack surface।

If request DTO does not contain:

customerId

status

unitPrice

total

then ordinary binding cannot accidentally give the client control over those values।

This is stronger than accepting them and saying:

we'll remember to ignore them

Make Invalid Operations Hard to Express

Good API design makes dangerous requests difficult or impossible to formulate।

For example:

POST /orders/{id}/pay

does not accept:

{
  "status": "PAID"
}

Client can request payment।

Client cannot directly choose the lifecycle result։

Likewise:

POST /orders/{id}/cancel

does not accept arbitrary new status।


Request DTOs Are an Authority Boundary

A useful question for every field:

If the client changes this field, should the server trust that value as authoritative?

If no, consider whether the field belongs in the request at all।

Examples:

CreateOrderRequest.customerId
→ No

CreateOrderRequest.total
→ No

CreateOrderItemRequest.unitPrice
→ No

CreateOrderItemRequest.quantity
→ Yes, as requested intent subject to validation

CreateProductRequest.price
→ Yes, for authorized admin, subject to domain validation

Response DTOs Are an Exposure Boundary

For every response field ask:

Does this client need this information as part of the public API?

Not:

Does the server happen to have this field?

Examples:

Order.status
→ useful

Order.total
→ useful

database version
→ probably not

raw provider response
→ no

access token
→ absolutely not

Never Return Secrets

Response DTOs must never include:

passwords

password hashes

API keys

access tokens

provider secrets

database credentials

This should be obvious, but explicit response models make accidental leakage less likely।


Avoid Returning Internal Exception Data

Error DTOs also must not expose:

stack traces

SQL

Java class names

internal file paths

Structured error responses will be covered later in this module।


Versioning Pressure

Once a response field is public, clients may depend on it։

Suppose we expose:

{
  "internalPaymentProvider": "provider-x"
}

and later switch providers।

Now infrastructure migration may accidentally become an API-breaking change։

This is another reason to expose application meaning rather than internal implementation details।


DTOs Reduce Coupling

Without explicit DTO:

Domain change
→ API changes

With explicit DTO:

Domain change
    ↓
mapping
    ↓
stable API contract

Similarly:

Database change

need not change API response shape at all।

This flexibility becomes important in production systems।


But Mapping Has a Cost

Separating DTOs means writing mapping code।

For example:

new ProductResponse(
        product.id().value(),
        product.name(),
        product.price()
);

This is additional code।

But for public API boundaries, that cost is usually small compared with the benefit of controlling the contract։


Don't Add Mapping Libraries Prematurely

For our current models, mappings are simple।

No need to immediately introduce:

MapStruct

ModelMapper

reflection-based mapping

Manual mapping is explicit and easy to debug։

If mapping becomes large and repetitive later, we can reconsider।


DTO Naming

Use names that communicate direction and operation:

CreateProductRequest

UpdateProductRequest

ProductResponse

SetInventoryRequest

InventoryResponse

CreateOrderRequest

CreateOrderItemRequest

OrderResponse

OrderItemResponse

Avoid generic:

ProductModel

OrderPayload

CommonData

ObjectDto

unless the name reflects a real shared concept।


Request vs Command Naming

Remember:

Request
→ HTTP boundary
Command
→ application operation input

For example:

CreateOrderRequest

may contain raw transport primitives։

CreateOrderCommand may contain:

ProductId

typed application values।

This distinction becomes useful as complexity grows।


Current Product DTO Direction

Conceptually:

public record CreateProductRequest(
        String name,
        BigDecimal price
) {
}
public record UpdateProductRequest(
        String name,
        BigDecimal price
) {
}
public record ProductResponse(
        String id,
        String name,
        BigDecimal price,
        boolean active
) {
}

Exact customer/admin response fields may be refined when implementing those endpoints concretely।


Current Inventory DTO Direction

public record SetInventoryRequest(
        int availableQuantity
) {
}
public record InventoryResponse(
        String productId,
        int availableQuantity
) {
}

No duplicated Product ID in update body।


Current Order DTO Direction

public record CreateOrderRequest(
        List<CreateOrderItemRequest> items
) {
}
public record CreateOrderItemRequest(
        String productId,
        int quantity
) {
}

Response:

public record OrderResponse(
        String id,
        String status,
        List<OrderItemResponse> items,
        BigDecimal total
) {
}
public record OrderItemResponse(
        String productId,
        int quantity,
        BigDecimal unitPrice,
        BigDecimal total
) {
}

These represent the current core contract without exposing server-controlled input fields।


DTOs We Intentionally Do Not Have

Current scope does not require:

CreateCustomerRequest

CustomerResponse

CreateOrderItemResponse as independent resource

UpdateOrderStatusRequest

SetOrderTotalRequest

ChangeOrderOwnerRequest

CreatePaymentRequest with invented provider fields

because those operations do not exist in our accepted v1 design।


Common Mistake 1 — One DTO for Everything

Create/update/read operations have different authority and field requirements।


Common Mistake 2 — Domain Entity as Request Body

Client gains access to fields that should remain server-controlled։


Common Mistake 3 — JPA Entity as Response

Persistence details leak into the public API।


Common Mistake 4 — Request Contains URI Identity Again

Creates ambiguity such as path P-100 but body P-200


Common Mistake 5 — Client Sends Price and Total During Order Creation

Pricing authority belongs to the backend।


Common Mistake 6 — Client Sends CustomerId for Own Order

Ownership comes from authenticated identity।


Common Mistake 7 — Client Sends OrderStatus

Lifecycle transitions are business operations, not arbitrary field assignments।


Common Mistake 8 — Provider DTO Returned Directly

External integration details leak into our public API।


Common Mistake 9 — Response Exposes Every Internal Field

Every public field creates coupling and potential security/compatibility cost։


Common Mistake 10 — Mapper Framework for Five Fields

Simple explicit mapping is usually easier until complexity actually justifies abstraction।


Request Model Review Checklist

For every request DTO, ask:

Does every field represent something
the caller is legitimately allowed to request?

Are server-owned values absent?

Is identity duplicated between URI and body?

Does authenticated identity come from security context?

Does the model reflect one clear operation?

Are transport and domain responsibilities separated?

Could this request accidentally bypass a business workflow?

Response Model Review Checklist

For every response DTO, ask:

Does the client actually need each field?

Are internal persistence details hidden?

Are provider-specific fields hidden?

Are secrets impossible to serialize through this model?

Is the JSON representation intentional?

Can domain implementation change
without unnecessarily changing this contract?

Is this response small enough for its use case?

Engineering Principle

The core principle:

Request models define what the client is allowed to ask for; response models define what the server intentionally exposes.

Another:

Do not mirror domain or database objects blindly across the HTTP boundary.

And:

A good DTO makes authority explicit: client-controlled intent goes in, server-controlled business state comes out.


Summary

In this lesson, we learned that:

  • Request and response DTOs are transport-boundary models.
  • DTOs are not domain Entities or persistence Entities.
  • Request and response models often need different fields.
  • CreateProductRequest should contain only client-controlled Product creation data.
  • Server-generated Product identity and lifecycle state belong in the response, not create input.
  • Partial Product update models require deliberate PATCH semantics.
  • Inventory target identity can remain in the URI while the request body carries only available quantity.
  • CreateOrderRequest should contain Product IDs and quantities only.
  • Authenticated Customer ownership must not come from the Order request body.
  • Authoritative unit price, Order status, and total must not come from the client.
  • OrderResponse may safely expose server-confirmed purchase-time prices and derived totals.
  • CreateOrderItemRequest and OrderItemResponse should remain separate because their authority and fields differ.
  • One universal DTO for create/update/list/detail often creates ambiguous contracts.
  • DTO reuse is appropriate only when the actual contract semantics are the same.
  • Response models should expose what clients need, not every field the server has.
  • Typed domain IDs can be mapped to simple transport representations such as strings.
  • Domain and JPA models should not be serialized directly by default.
  • Java records are a natural fit for immutable request/response DTOs.
  • Records provide immutability but do not automatically provide validation.
  • DTOs should stay near their capability's Handler rather than in a global dumping-ground package.
  • Application Commands are distinct from HTTP Request DTOs.
  • Manual mapping is appropriate while mappings remain simple.
  • Explicit DTOs reduce coupling between API, domain, persistence, and external integrations.
  • DTO design forms both an authority boundary for requests and an exposure boundary for responses.

Next lesson:

HTTP Status Codes

There we will define how our Product, Inventory, and Order endpoints use 200, 201, 204, 400, 401, 403, 404, 409, and 5xx responses, and distinguish malformed requests, missing resources, authorization failures, business-state conflicts, and unexpected server failures consistently.