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!