Combining decorators for easy data manipulation
Hello my fellow confined nerds lol
More seriously I wish you all to be safe and sound 😷
I made a few experiments for a couple of days and I wanted to lay down my thoughts on data manipulation with TypeScript decorators in Nest.js.
Concepts
Whether writing websites, softwares, apps or anything involving sending data back and forth there’s always this chore to serialize/deserialize from one format to another, whether it be :
- JS object to/from JSON
yeah, quite straightforward - plain JS object to/from typed JS class object
yeah, cumbersome heh - Java POJO to/from JSON
I see Hibernate/Jackson users smile at the back - and so on : AMF, Protobuf, XML, YML, etc…
Now aside from serialization/deserialization, there’s also this fact that some data can cross boundaries or not.
Boundaries can be anything like your backend as opposed to your frontend for example. So there’s also this one another chore like sanitization and hydration :
- sanitization : best example here would be the password from your user which is typically the data that you want to strip away from being inadvertently sent to your frontend.
- hydration : reverse process, when sometimes you receive user data from the frontend and want to feed its missing properties from database to carry out some more logic.
And so what ? So it’s a lot of boilerplate.
And me dumb lazy ass hates dat 🙄
Let’s code
I’m happy to share some experience with Nest.js, class-validator and class-transformer which makes it way more easier and less verbose.
Requirements
This sample assumes that you’re already a bit familiar with these librairies :
typescript
^3.7.4nest.js
^7.0.0typeorm
^0.2.24class-transformer
^0.2.3class-validator
^0.12.0-rc.0moment
^2.24.0chance
^1.1.4
Architecture
In a nutshell, request/response can be seen as a pipe flow :
Entity
The whole trick lies here for the most part. By taking advantages of typeorm, class-validator and class-transformer to accurately define what our data will look like in different contexts :
- as a database representation
- as a typed JS class object
- as a plain JS object
available as a gist here
available as a gist here
Here I will only focuson
, user
and owner
:
on
:@Column
with transformer handles the conversion between database to JS representations, while both@Transform
handle the conversion between plain JS object and typed JS object (notice the toClassOnly and toPlainOnly). It is customized to handle conversion between a moment object and a string representation while keeping all the informations (including milliseconds, how nice is that !).user
: this illustrates when a data is meant to exist solely in backend, as it will be excluded when transformed into a plain JS object with@Exclude
. It will just allow for conveniently loading relation (joint table in database) to retrieve theUser
owning theNote
with@ManyToOne
. The key for this joint isowner
as specified with@JoinColumn
.owner
: this data is the key to theUser
id whose thisNote
belongs to. It will be exposed when transformed into a plain JS object with@Expose
.
The other properties are pretty much self-explanatory, don’t hesitate to ask in the comments for more information or directly check out the libraries API.
Interceptor
IOInterceptor
will automate transforming JS class object to plain JS object with classToPlain
on each output, which of course will also sanitize the data marked with @Expose
and @Exclude
as specified earlier too !
available as a gist here
Controller
By adding the previously created IOInterceptor
to the controller itself, we ensures that every data send back to the outer world will first be transformed into a plain JS object.
Nest.js will then turn it into JSON at the very end
available as a gist here
Seeds
In order to test our sample app in real conditions, we first want to be able to seed our test database with some entries.
available as a gist here
Here we’re creating random data with chance to allow for some entropy. Notes are also randomly allocated to some previously created users.
⚠️ I got befuddled at first when implementing because I forgot that a moment object can automatically be created from a valid date string, hence moment.utc(chance.date({ year: 1983 }).format(TIME)
(while casting it to unknown
and moment.Moment
is just a means to let TypeScript compiler allows it).
Thanks to cojack for pointing this out in this issue.
Integration test
Now that everything is ready, let’s test it :
available as a gist here
As you can see, every time we run the test we previously seed our database with random data and ensures that these are the exact same ones we receive from the backend afterwards.
I intentionally skipped some files to focus on the main points but you can find the whole repository here :
As you can guess, from that on data validation could also be easily implemented with Nest.js Pipe, check out my previous article on this :
Wish you all a good day 🖐
As always,
don’t hesitate to comment 💬
or show some support 👏