Serialization pattern: class-transformer
Consistent serialization and deserialization across the API boundary so that Date values and class instances (including nested models) are correctly sent as JSON and, on the frontend, restored as real class instances with proper types.
Overview
The stack uses class-transformer (see stack rule serialization under stack/shared/): the backend serializes response data with instanceToPlain before sending; the frontend may deserialize success data with plainToInstance when a DTO class is provided, and serializes request bodies with instanceToPlain. Shared DTO/model classes define the shape with decorators so nested models and Dates behave correctly in both directions.
Shared models (DTOs)
DTO and model classes live in shared (e.g. shared/src/dto/ or shared/src/models/). They use class-transformer decorators so serialization and deserialization are predictable:
- Nested classes: Use
@Type(() => ChildClass)on properties that are other DTOs so they are instantiated when deserializing (e.g. on the frontend). - Date fields: Use two
@Transformdecorators so outbound is ISO string and inbound isDate:@Transform(({ value }) => value?.toISOString?.() ?? value, { toPlainOnly: true })@Transform(({ value }) => value != null ? new Date(value) : undefined, { toClassOnly: true })Avoid relying on@Type(() => Date)alone when you need consistent ISO strings; the dual-@Transformpattern gives explicit control.
- Visibility: Use
@Expose()and@Exclude()as needed so only intended properties are serialized or deserialized.
Shared package depends on class-transformer and, if it contains these DTOs, must have experimentalDecorators and emitDecoratorMetadata enabled in tsconfig.json.
Backend
The ResponseInterceptor serializes the handler return value before wrapping it in the envelope: it runs instanceToPlain(data) so that class instances and Date fields become plain JSON (e.g. Dates as ISO strings, nested classes as plain objects). Controllers and services return domain objects (class instances or plain); the interceptor ensures the wire format is always plain. Services that map from an ORM (e.g. Prisma) can use plainToInstance(DtoClass, plain) so controllers return class instances that the interceptor then serializes. The backend depends on class-transformer and uses it in the interceptor.
Frontend
- Responses: When the caller needs
dataas class instances (e.g.UserDtowithcreatedAt: Dateand nestedProfileDto), they pass the DTO class via the request config (e.g.transformTo: UserDto).handleRequestthen runsplainToInstance(Cls, envelope.data)on success and returns the envelope withdataas that class instance. When no class is passed,dataremains the raw plain object (current behavior). - Request bodies: When calling
post,put, orpatchwith a body, the client serializes the body withinstanceToPlain(data)before sending so class instances and Dates are sent in the same format the backend expects. Callers can pass either plain objects or class instances; the client normalizes to plain.
The frontends/shared package depends on class-transformer and uses it in lib/api.ts for plainToInstance (success responses, when transformTo is set) and instanceToPlain (request bodies).
References
- Stack rule:
serialization(understack/shared/) for the technology and decorator usage. - Pattern
responsesfor the envelope; patternrequestsfor request handling and validation. - Backend:
controller-layer,framework. Frontend:http-client.
Implementation
The boilerplate implements this pattern as follows.
- Shared: The shared package lists class-transformer as a dependency. If the shared package contains DTO classes with decorators,
shared/tsconfig.jsonhasexperimentalDecorators: trueandemitDecoratorMetadata: true. DTOs are added undershared/src/(e.g.dto/ormodels/) as needed; the generation skill does not add an example DTO by default. - Backend: The backend lists class-transformer as a dependency. ResponseInterceptor (
backend/src/interceptors/response.interceptor.ts) importsinstanceToPlainfromclass-transformerand, before building the envelope, setsdatatoinstanceToPlain(data)whendatais present (otherwise leaves it unchanged). This ensures all success response payloads are plain JSON. - Frontend (frontends/shared): The frontends/shared package lists class-transformer as a dependency. lib/api.ts imports
instanceToPlainandplainToInstance. The request config type is extended with an optional transformTo (a class constructor). On success, iftransformTois provided,envelope.datais replaced withplainToInstance(transformTo, envelope.data). Forpost,put, andpatch, when a body is provided, the body is serialized withinstanceToPlain(data)before being sent. ThushandleRequest.get<UserDto>(url, { transformTo: UserDto })returnsPromise<ApiResponse<UserDto>>withdataas aUserDtoinstance when the response is successful.