-
Notifications
You must be signed in to change notification settings - Fork 1
08_NestJS Code Guidelines
This page gives basic guidance on developing NestJS backend functionalities. It's highly recommended to read the NestJS Documentation beforehand.
The backend folder structure is as follows (roughly):
src
├── config
├── flox
│ ├── core
│ └── modules
│ ├── file
│ ├── user
│ └── ...
├── i18n
├── modules
│ ├── garage
│ ├── dossier
│ └── ...
├── app.module.ts // Application root module, must include other modules to be used
├── main.ts // Main setup file
├── schema.gql // GraphQL schema, auto-generated
...
Note that we have a folder modules
that contains all our customer specific NestJS Modules. The modules contained in flox/modules are not project specific Modules, but generic modules (e.g. User, Roles, E-mail, helpers, etc.) that may be used by project specific modules.
This distinction is made such that common functionality is separated from customer modules and can be reused without creating codependencies between modules.
A module usually goes along with one or multiple corresponding table
s in the database; although exceptions may apply.
When adding new functionalities, the suitable location can be determined by looking at who will need to consume this functionality. If it's specific to the application (e.g. sending an e-mail containing dossier documents
), it should be part of that module (e.g. dossier
). On the other hand, the underlying generic functionality (e.g. sending an e-mail
) should be part of a flox/modules(e.g. email
).
When adding a new custom module, don't forget to add it to app.module.ts
.
A module's folder structure must follow the following convention.
If only either args
or input
are needed, the corresponding files can be directly placed in the dto
folder, and subfolders omitted.
user
├── dto // Data transfer objects
│ ├── args // Argument objects for queries
│ │ └── get-user.args.ts
│ └── input // Input objects for mutations
│ └── create-user.input.ts
├── entities // Database entities
│ └── user.entity.ts
│
├── user.module.ts
├── user.controller.ts // If needed (REST)
├── user.resolver.ts // If needed (GraphQL)
├── user.service.ts
├── ... (test files, if any)
Authorization and Authentication are an integral part of designing secure, well-determined backend functionalities. By default, we should use a restrictive access policy on backend functionalities. This means that, unless explicitly specified through the use of custom decorators, a functionality will be inaccessible to a user group. The following basic decorators are provided by the bootstrap repository:
@Public() // Accessible to everyone, even public traffic
@LoggedIn() // Accessible to any user logged in through Cognito
@AdminOnly() // Accessible to users with the 'admin' role
@Roles([...]) // Accessible to the given roles
Individual applications may define further decorators for other roles.
The handling of roles is up to the application itself, and how it implements them. The handling of login state, on the other hand, is part of Flox. Simply put, once a user has successfully logged in through Cognito, a JSON Web Token (JWT) will be attached to each of their requests. From this token, the user's Cognito ID is accessible, and can be used for further authorization checks.
In order to gain access to this JWT, the @CurrentUser
decorator can be used.
Note that this token is NOT the database entry corresponding to the user, and thus does not make any statements about the user's roles or permissions. A valid JSON web token (as verified automatically in our backend) however guarantees that the stated user signed in through Cognito. Thus, we can use the UUID contained within the token to fetch the corresponding database user in the backend.
It's often nontrivial to determine what parts of e.g. a mutation belong to the Resolver, and what parts should belong to the service.
Simply put, Authorization and Authentication are responsibilities of the Resolver, while functionality is the service's responsibility. This distinction helps us write reusable service functions, oftentimes allowing us to write multiple Mutations or Queries that use the same service functions in different ways, or resolver functions that utilize multiple service functions without repeating authorization checks.
Consider the following minimal NestJS Resolver function:
@AdminOnly()
@Mutation(() => User)
async update(
@Args('updateUserInput') updateUserInput: UpdateUserInput,
): Promise<User> {
return this.usersService.update(updateUserInput);
}
Note that none of the actual functionality is part is part of the resolver's function; instead, it simply calls this.usersService.update
. This helps us reuse service functions; e.g. if we want to have a Mutation updateUserName
that can only change the user's name (and may be accessible to different groups of users), we can simply reuse the userService
's update
function.
This, however, does NOT mean that resolver functions are not allowed to have their own functionality. While some parts like role authorization can be covered through decorators (e.g. @AdminOnly()
), other authorization measures can be part of the Resolver function.
Consider, for example, a simple Mutation that can modify a File, but only if the user accessing it is older than 25 years:
@AnyRole()
@Mutation(() => File)
async updateFileIfUserIsOldEnough(
@Args('updateFileInput') updateFileInput: UpdateFileInput,
@CurrentUser() user: Record<string, string>,
): Promise<File> {
if(currentUser.age > 25){
return this.filesService.update(updateFileInput);
}
throw new Error('User is too young')
}
Instead of creating a specialized function within the userService updateIfUserOldEnough
, we use its generic update
function, making any conditional parts that are specific to this mutation part of the resolver's function.
Service functions should be able to be used in any number of resolver functions without needing to fulfill additional conditions, as long as all parameters are specified correctly Resolver functions should take full responsibility to ensure they are not executed by users that are not authorized to do so.