I would like to get into creating REST APIs with NestJs and I'm not sure how to setup scalable layer communication objects.
So from the docs on how to get started I come up with a UsersController
dealing with the HTTP requests and responses, a UsersService
dealing with the logic between the controller and the database accessor and the UsersRepository
which is responsible for the database management.
I use the TypeORM package provided by NestJs so my database model would be
@Entity('User')
export class UserEntity extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
username: string;
@Column()
passwordHash: string;
@Column()
passwordSalt: string;
}
but as you might know this model has to be mapped to other models and vice versa because you don't want to send the password information back to the client. I will try to describe my API flow with a simple example:
Controllers
First I have a controller endpoint for GET /users/:id
and POST /users
.
@Get(':id')
findById(@Param() findByIdParamsDTO: FindByIdParamsDTO): Promise<UserDTO> {
// find user by id and return it
}
@Post()
create(@Body() createUserBodyDTO: CreateUserBodyDTO): Promise<UserDTO> {
// create a new user and return it
}
I setup the DTOs and want to validate the request first. I use the class-validator package provided by NestJs and created a folder called RequestDTOs. Finding something by id or deleting something by id via url parameters is reusable so I can put this into a shared folder for other resources like groups, documents, etc.
export class IdParamsDTO {
@IsUUID()
id: string;
}
The POST request is user specific
export class CreateUserBodyDTO {
@IsString()
@IsNotEmpty()
username: string;
@IsString()
@IsNotEmpty()
password: string;
}
Now the controller input gets validated before executing business logic. For the responses I created a folder called ResponseDTOs but currently it only contains the database user without its password information
export interface UserDTO {
id: string;
username: string;
}
Services
The service needs the bundled information from the params and the body.
public async findById(findByIdBO: FindByIdBO): Promise<UserBO> {
// ...
}
public async create(createBO: CreateBO): Promise<UserBO> {
// ...
}
The GET request only needs the ID, but maybe it's still better to create a BO because you might want to switch from string IDs to integers later. The "find by id" BO is reusable, I moved it to the shared directory
export interface IdBO {
id: string;
}
For the user creation I created the folder RequestBOs
export interface CreateBO {
username: string;
password: string;
}
Now for the ResponseBOs the result would be
export interface UserBO {
id: string;
username: string;
}
and as you will notice this is the same like the UserDTO. So one of them seems to be redundant?
Repositories
Lastly I setup the DAOs for the repositories. I could use the auto-generated user repository and would deal with my database model I mentioned above. But then I would have to deal with it within my service business logic. When creating a user I would have to do it within the service and only call the usermodel.save
function from the repository.
Otherwise I could create RequestDAOs
The shared one..
export interface IdDAO {
id: string;
}
And the POST DAO
export interface CreateDAO {
username: string;
password: string;
}
With that I could create a database user within my repository and map database responses with ResponseDAOs but this would always be the whole database user without the password information. Seems to generate a big overhead again.
I would like to know if my approach using 3 request and 3 response interfaces is way too much and can be simplified. But I would like to keep a flexible layer because I think those layers should be highly independent... On the other hand there would be a huge amount of models out there.
Thanks in advance!
I handle this by having a single class to represent a User (internally and externally) with the class-transformer
library (recommended by NestJs) to handle the differences between the exposed user and the internal user without defining two classes.
Here's an example using your user model:
Since this user class is saved to the database, I usually create a base class for all the fields that every database object expects to have. Let's say:
export class BaseDBObject {
// this will expose the _id field as a string
// and will change the attribute name to `id`
@Expose({ name: 'id' })
@Transform(value => value && value.toString())
@IsOptional()
// tslint:disable-next-line: variable-name
_id: any;
@Exclude()
@IsOptional()
// tslint:disable-next-line: variable-name
_v: any;
toJSON() {
return classToPlain(this);
}
toString() {
return JSON.stringify(this.toJSON());
}
}
Next, our user will expend this basic class:
@Exclude()
export class User extends BaseDBObject {
@Expose()
username: string;
password: string;
constructor(partial: Partial<User> = {}) {
super();
Object.assign(this, partial);
}
}
I'm using a few decorators here from the class-transformer
library to change this internal user (with all the database fields intact) when we expose the class outside of our server.
@Expose
- will expose the attribute if the class-default is to exclude@Exclude
- will exclude the property if the class-default is to expose@Transform
- changes the attribute name when 'exporting'This means that after running the classToPlain
function from class-transformer
, all the rules we defined on the given class will be applied.
NestJs
have a decorator you add to make sure classes you return from controller endpoints will use the classToPlain
function to transform the object, returning the result object with all the private fields omitted and transformations (like changing _id
to id
)
@Get(':id')
@UseInterceptors(ClassSerializerInterceptor)
async findById(@Param('id') id: string): Promise<User> {
return await this.usersService.find(id);
}
@Post()
@UseInterceptors(ClassSerializerInterceptor)
async create(@Body() createUserBody: CreateUserBodyDTO): Promise<User> {
// create a new user from the createUserDto
const userToCreate = new User(createUserBody);
return await this.usersService.create(userToCreate);
}
@Injectable()
export class UsersService {
constructor(@InjectModel('User') private readonly userModel: Model<IUser>) { }
async create(createCatDto: User): Promise<User> {
const userToCreate = new User(createCatDto);
const createdUser = await this.userModel.create(userToCreate);
if (createdUser) {
return new User(createdUser.toJSON());
}
}
async findAll(): Promise<User[]> {
const allUsers = await this.userModel.find().exec();
return allUsers.map((user) => new User(user.toJSON()));
}
async find(_id: string): Promise<User> {
const foundUser = await this.userModel.findOne({ _id }).exec();
if (foundUser) {
return new User(foundUser.toJSON());
}
}
}
Because internally we always use the User class, I convert the data returned from the database to a User class instance.
I'm using @nestjs/mongoose
, but basically after retrieving the user from the db, everything is the same for both mongoose
and TypeORM
.
With @nestjs/mongoose
, I can't avoid creating IUser
interface to pass to the mongo Model
class since it expects something that extends the mongodb Document
export interface IUser extends mongoose.Document {
username: string;
password: string;
}
When GETting a user, the API will return this transformed JSON:
{
"id": "5e1452f93794e82db588898e",
"username": "username"
}
Here's the code for this example in a GitHub repository.
If you want to see an example using typegoose
to eliminate the interface as well (based on this blog post), take a look here for a model, and here for the base model
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With