Data Transfer Objects
Models define how data looks at storage (rest) DTO (Data Transfer Object) defines how data looks in transit.
It is a contract of how data is shared between different layers (frontend and backend, controller and service, etc.) of an application.
Between client and server, DTO contracts can be shared as OpenAPI specfication.
components:
schemas:
UserDTO:
type: object
required:
- id
- displayName
- email
properties:
id:
type: string
format: uuid
displayName:
type: string
email:
type: string
format: email
then, the frontend and backend use this contract to make sure their data structures match perfectly.
In backend,
// Internal Database Entity (Hidden from client)
interface UserEntity {
id: string;
firstName: string;
lastName: string;
email: string;
passwordHash: string; // Sensitive data
createdAt: Date;
}
// The DTO matching the OpenAPI contract
interface UserDTO {
id: string;
displayName: string;
email: string;
}
// Controller handling the API request
app.get('/api/users/:id', async (req, res) => {
const user: UserEntity = await database.findUser(req.params.id);
// Mapping entity to DTO
const userDTO: UserDTO = {
id: user.id,
displayName: `${user.firstName} ${user.lastName}`,
email: user.email
};
res.json(userDTO);
});
in the frontend
// Client-side Model (Matches the OpenAPI contract)
interface UserDTO {
id: string;
displayName: string;
email: string;
}
// Component fetching and rendering the data
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<UserDTO | null>(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then((data: UserDTO) => setUser(data)); // Safely typed
}, [userId]);
if (!user) return <div>Loading...</div>;
return (
<div>
<h1>{user.displayName}</h1> {/* Correctly uses mapped display name */}
<p>{user.email}</p>
</div>
);
}
if you’re using DTO between backend layers (controller, service) this DTO can be just interfaces that define how data is passed between layers.
the implementation of these contracts are enforced just just by the developers. There used to be no guardrails present. But things have changed. Guardrails have been created, based on the tool you’re using.
- on the design pattern level, DTOs can be considered just design patterns. No enforcement.
- build time enforcement (using CI / CD pipelines, code generation, etc.) to ensure that the code changes on both sides when the DTO changes.
- runtime, protocol enforcement:
DTO is a contract defined (during design time) DTO object is an object created at runtime that adheres to that contract.
Advantages
- confidentiality: it allows you to control exactly what data is between different layers (ex: client and server)
- backward compatibility and versioning: API requirements may change as your app grows. Using DTO, you can adapt your API responses without altering the underlying models
Further Reading: