Building REST APIs
Request and Response Models
আপনি একটি 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.
CreateProductRequestshould 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.
CreateOrderRequestshould 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.
OrderResponsemay safely expose server-confirmed purchase-time prices and derived totals.CreateOrderItemRequestandOrderItemResponseshould 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.