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.

  1. on the design pattern level, DTOs can be considered just design patterns. No enforcement.
  2. build time enforcement (using CI / CD pipelines, code generation, etc.) to ensure that the code changes on both sides when the DTO changes.
  3. runtime, protocol enforcement:
  • grpc,
  • shared types: if you’re using typescript, you can enforce these contracts by defining them as shared types @shared/types in a monorepo.
  • graphql etc.

DTO is a contract defined (during design time) DTO object is an object created at runtime that adheres to that contract.

Advantages

  1. confidentiality: it allows you to control exactly what data is between different layers (ex: client and server)
  2. 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: