Transformers
Transformers allow you to modify request and response data during the CRUD operation lifecycle. They are useful for data enrichment, format conversion, and business logic that doesn’t belong in entities.
Request Transformers
Transform incoming request data before processing:
import {
ApiController,
EApiRouteType,
EApiDtoType,
EApiControllerRequestTransformerType,
TRANSFORMER_VALUE_DTO_CONSTANT
} from "@elsikora/nestjs-crud-automator";
@ApiController<PostEntity>({
entity: PostEntity,
routes: {
[EApiRouteType.CREATE]: {
request: {
transformers: {
[EApiDtoType.BODY]: [
{
key: "authorId",
type: EApiControllerRequestTransformerType.DYNAMIC,
value: TRANSFORMER_VALUE_DTO_CONSTANT.AUTHORIZED_ENTITY,
shouldSetValueEvenIfMissing: true,
},
{
key: "createdAt",
type: EApiControllerRequestTransformerType.STATIC,
value: () => new Date(),
},
],
},
},
},
},
})
Transformer Types
Static Transformers
Set a fixed value or value from a function:
{
key: "status",
type: EApiControllerRequestTransformerType.STATIC,
value: "draft",
}
// Or with a function
{
key: "uuid",
type: EApiControllerRequestTransformerType.STATIC,
value: () => crypto.randomUUID(),
}
Dynamic Transformers
Extract values from request context:
// Set from authenticated user
{
key: "createdById",
type: EApiControllerRequestTransformerType.DYNAMIC,
value: TRANSFORMER_VALUE_DTO_CONSTANT.AUTHORIZED_ENTITY,
shouldSetValueEvenIfMissing: true,
}
// Set from request headers
{
key: "clientVersion",
type: EApiControllerRequestTransformerType.DYNAMIC,
value: (context) => context.headers["x-client-version"],
}
// Set from request IP
{
key: "ipAddress",
type: EApiControllerRequestTransformerType.DYNAMIC,
value: (context) => context.ip,
}
Available Dynamic Values
Built-in dynamic transformer values:
// Authenticated user entity
TRANSFORMER_VALUE_DTO_CONSTANT.AUTHORIZED_ENTITY
// Request IP address
TRANSFORMER_VALUE_DTO_CONSTANT.IP
// Request headers
TRANSFORMER_VALUE_DTO_CONSTANT.HEADERS
// Full authentication context
TRANSFORMER_VALUE_DTO_CONSTANT.AUTHENTICATION_REQUEST
Response Transformers
Transform outgoing response data:
routes: {
[EApiRouteType.GET]: {
response: {
transformers: [
{
key: "fullName",
value: (entity: UserEntity) =>
`${entity.firstName} ${entity.lastName}`,
},
{
key: "isAdult",
value: (entity: UserEntity) => entity.age >= 18,
},
],
},
},
}
Transform Based on User Role
Apply different transformations based on user role:
routes: {
[EApiRouteType.GET]: {
response: {
transformers: [
{
key: "sensitiveData",
value: (entity: UserEntity, context: any) => {
const user = context.authenticationRequest?.user;
if (user?.role === "admin") {
return entity.sensitiveData;
}
return null; // Hide from non-admins
},
},
],
},
},
}
Complex Transformations
Perform complex data transformations:
routes: {
[EApiRouteType.GET_LIST]: {
response: {
transformers: [
{
key: "statistics",
value: async (entity: PostEntity) => {
return {
viewCount: entity.views?.length || 0,
likeCount: entity.likes?.length || 0,
commentCount: entity.comments?.length || 0,
engagement: calculateEngagement(entity),
};
},
},
],
},
},
}
Conditional Transformations
Apply transformations conditionally:
{
key: "discountedPrice",
value: (entity: ProductEntity) => {
if (entity.onSale && entity.discountPercent) {
return entity.price * (1 - entity.discountPercent / 100);
}
return entity.price;
},
}
Nested Object Transformations
Transform nested objects:
{
key: "address",
value: (entity: UserEntity) => {
if (!entity.address) return null;
return {
...entity.address,
formatted: `${entity.address.street}, ${entity.address.city}, ${entity.address.country}`,
};
},
}
Array Transformations
Transform array properties:
{
key: "tagNames",
value: (entity: PostEntity) => {
return entity.tags?.map(tag => tag.name) || [];
},
}
Using class-transformer
Leverage class-transformer for advanced transformations:
import { Transform, Exclude } from "class-transformer";
@Entity("users")
export class UserEntity {
@Column()
@Transform(({ value }) => value.toLowerCase())
email: string;
@Column()
@Exclude() // Never include in responses
password: string;
@Column()
@Transform(({ value }) => value.trim())
username: string;
@Column({ type: "jsonb" })
@Transform(({ value }) => JSON.stringify(value))
metadata: Record<string, any>;
}
Pre-Processing in Transformers
Pre-process data before validation:
routes: {
[EApiRouteType.CREATE]: {
request: {
transformers: {
[EApiDtoType.BODY]: [
{
key: "email",
type: EApiControllerRequestTransformerType.STATIC,
value: (body: any) => body.email?.toLowerCase().trim(),
},
{
key: "tags",
type: EApiControllerRequestTransformerType.STATIC,
value: (body: any) =>
body.tags?.map((tag: string) => tag.toLowerCase()) || [],
},
],
},
},
},
}
Async Transformers
Perform async operations in transformers:
{
key: "author",
value: async (entity: PostEntity, context: any) => {
const userService = context.services.user;
return await userService.get({ where: { id: entity.authorId } });
},
}
Removing Properties
Remove properties from response:
{
key: "password",
value: () => undefined, // Removes the property
}
Global Transformers
Apply transformers to all routes:
@ApiController<UserEntity>({
entity: UserEntity,
globalTransformers: {
request: {
[EApiDtoType.BODY]: [
{
key: "updatedAt",
type: EApiControllerRequestTransformerType.STATIC,
value: () => new Date(),
},
],
},
response: [
{
key: "timestamp",
value: () => Date.now(),
},
],
},
routes: {
// ... route configurations
},
})
Transformer Order
Transformers execute in the following order:
- Global request transformers
- Route-specific request transformers
- Validation
- Service method execution
- Route-specific response transformers
- Global response transformers
Performance Considerations
Avoid Heavy Operations
// Bad: Expensive operation in transformer
{
key: "relatedPosts",
value: async (entity: UserEntity) => {
// This runs for every item in a list
return await postService.getMany({
where: { authorId: entity.id }
});
},
}
// Good: Use relations instead
request: {
relations: {
shouldLoadRelations: true,
relationsToLoad: ["posts"],
},
}
Cache Expensive Computations
const cache = new Map();
{
key: "expensiveCalculation",
value: (entity: ProductEntity) => {
const cacheKey = `calc-${entity.id}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const result = performExpensiveCalculation(entity);
cache.set(cacheKey, result);
return result;
},
}
Next Steps
- Validation - Validate transformed data
- Subscriber System - Alternative approach for data transformation
- Core Concepts - Controllers - Controller transformer configuration