Which fields will you allow to survive?

NestJS-Serialization/Hidden Fields/Role-Based Serialization

Ed Stephenson

--

I’ve recently been trying to figure out the best way to hide sensitive data from consumers of my applications.

Typically, a good example of this is hiding the password field on a user model or “Entity”:

export class UserEntity {
firstname: string;
lastname: string;
email: string;
password?: string;
}

So, using NestJS, how do we hide the password field across the whole application?

Incredibly usefully, NestJS serializes all classes on the response: https://docs.nestjs.com/techniques/serialization.

And they make use of the class-transformer package to make this happen: https://www.npmjs.com/package/class-transformer.

Enabling the ClassSerializerInterceptor

In order for this to work , you need to enable the ClassSerializerInterceptor. You can do this in two ways:

Controller Implementation

controller.ts@UseInterceptors(ClassSerializerInterceptor)

Global Implementation

main.tsapp.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));

Basic Serialization

So all we have to do is add an @Exclude() decorator to the fields we want to exclude from the response:

export class UserEntity {
firstname: string;
lastname: string;
email: string;
@Exclude()
password?: string;
}

Simple right? Not quite…

If you test this, you’ll find you now can’t access the password field anywhere in your app. So if you want to ONLY exclude it from the response, but still be able to access it from your class inside your app, you need to make a small tweak:

export class UserEntity {
firstname: string;
lastname: string;
email: string;
@Exclude({ toPlainOnly: true })
password?: string;
}

Ta-da!

Role Based Serialization

Now we can look at Role based exclusion. By this, I mean, if you have a public and admin exposure to your app.

We’ll use aProductEntity for this example which could look like this:

export class ProductEntity {
name: string;
price: number;
cost: number;
}

Now, if you’re anything like me, you won’t want your customers knowing your costs! So let’s secure our ProductEntity by using the following decorators:

export class ProductEntity {
name: string;
@Expose({ groups: ['role:customer', 'role:admin'] })
price: number;
@Expose({ groups: ['role:admin'] })
cost: number;
}

Then, we need to use the @SerializeOptions() decorator in our controller to inform class-transformer of the role it needs to take into account on when transforming the class to a POJO:

CustomerController

@Controller('/api/products')
export class CustomerProductsController {
constructor(
private readonly productService: ProductService
) {}

@SerializeOptions({
groups: ['role:customer'],
})
@Get(':id')
async getAssignedPurchaseOrders(
@Param('id') productId: number,
): Promise<IPurchaseOrder[]> {
return this.productService.findById(productId);
}
}

AdminController

@Controller('/admin/products')
export class AdminProductsController {
constructor(
private readonly productService: ProductService
) {}

@SerializeOptions({
groups: ['role:admin'],
})
@Get(':id')
async getAssignedPurchaseOrders(
@Param('id') productId: number,
): Promise<IPurchaseOrder[]> {
return this.productService.findById(productId);
}
}

Short of creating an enum to define all possible roles across your application, All done.

Working with Generics

I had an issue recently where I was working with a PaginationResponse where an unspecified Entity array would form the results property of that response. The issue was that class-transformer wasn’t able to see an Entity there at runtime, and so the Entity class wasn’t being transformed.

The class-transformer package talks about this issue here. Following their suggestions, I settled on the below solution:

export class PaginationResponse<T extends BaseEntity<T>> {  @Exclude()
private type: any;
constructor(
data: Partial<PaginationResponse<T>>,
type: typeof BaseEntity
) {
Object.assign(this, data);
this.type = type;
}
@Type((options) => {
return (options.newObject as PaginationResponse<T>).type;
})
results: T[];
page: number;
totalPages: number;
start: number;
end: number;
pageLength: number;
filteredCount: number;
totalCount: number;
}

I construct my PaginationResponse in my Database Provider like so:

base-database.provider.tsabstract getEntity(): typeof BaseEntity;async all(...args) {  ... DB CODE  return new PaginationResponse(
{
results: res,
page,
totalPages,
start,
end: start + res.length,
pageLength: res.length,
filteredCount,
totalCount,
},
this.getEntity(),
);
}

Then I can specify which Entity to use in the class which extends as so:

purchase-order.provider.ts extending base-database.provider.tsgetEntity() {
return PurchaseOrderEntity;
}

Usual Caveats

I’m a self-taught programmer. If you have any questions/suggestions about my work or just want a chat, please reach out!

--

--

Ed Stephenson

Embracing the unconventional. Striving to make a difference, one impatient step at a time. Works at Flowflex UK. Supports Entrepreneurism, Manufacturing & Tech.