- Table of Contents
- Technologies
- Installation & Setup
- Running the app
- Generate Modules
- Code First approach with TypeORM
- Generate Entity & Repository
- How to use Repository
- Migration
- Seeding Data
- Module Structure
- Module Naming Convention
- RBAC (Role-Based Access Control)
- Upload media file
- Format of API Response
- Public Folder
- Git Branches
- API Documentation
- API Testing
- MacOS:
$ brew install redis
- Linux:
$ sudo apt-get install redis-server
- Windows: HERE
Or you can use Docker Compose, it will come with Redis and MySQL:
$ docker-compose up -d
$ npm install
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
We will follow the RESTful API naming and HTTP methods convention.
Read more about RESTful API naming convention
Open terminal, go to project folder and run this command:
$ npm run generate:module
Then, input module name and it will generate module for you.
In case you can't run the script, you can run the command below to generate module:
node_modules/.bin/nest g resource <put module name here> modules --no-spec
Read this carefully before you start coding.
We will use Code First approach with TypeORM.
That means we will write entity file first, then TypeORM will generate table for us.
Generate table using synchronize
feature of TypeORM.
You have to write specific column type in entity file.
The first way to generate table is using synchronize
feature of TypeORM.
- First, you need to write entity file in
src/database/typeorm/entities
folder. - Then, set
DATABASE_SYNCHRONIZE=true
in.env
file. - Finally, run the app and TypeORM will generate table for you.
- After that, you can set
DATABASE_SYNCHRONIZE=false
in.env
file to disable synchronize feature.
The second way to generate table is using migration
feature of TypeORM.
- You have to write entity then write migration file with SQL query to generate table.
- Then follow the steps in Migration section to run migration file.
The third way to generate table is using schema:sync
command of TypeORM. (Recommended)
- First, you need to write entity file in
src/database/typeorm/entities
folder. - Then, run this command to generate table:
$ npm run db:sync
- It works like
synchronize
feature of TypeORM. But it easier to use.
IMPORTANT:
- With this approach, you can use with
migration
feature of TypeORM.
Caution: If you change column config in entity file, TypeORM will drop old column and create new column for you. So, you will lose all data in that column or even the table. So be careful when you change column config in entity file. The properties of entity can change to whatever you want, but it's better to follow the naming convention. - When you use this approach, you must follow this approach for all entities. Don't use
migration
feature for some entities andsynchronize
feature for other entities. It will cause error. - When you use
migration
feature, you must setDATABASE_SYNCHRONIZE=false
in.env
file. And never usesynchronize
feature anymore. It will lose all data in database. Choose one of these 2 features and stick with it.
Read more about TypeORM Entity
Or you can just ignore this approach and use Migration feature instead.
There are some commands to help you generate and run migration file. Check out the package.json file to see the commands.
Read more about TypeORM Migration
But I would recommend you to write a very detail entity file. Especially the relation between entities. It will help you a lot in the future.
We use Relation in Entity to create relation between entities.
It will help us to query data from database easier. And it will create the foreign key between tables in database.
But we don't need to create foreign key in database. It will cause error when you try to delete data in database or sync database. And will slow down the performance of database. If you want to keep constraint, you can do it in your code.
But we just want to create the relation, not the foreign key. So we need to set createForeignKeyConstraints: false
in RelationOptions.
This setting will create the relation between entities, but not create the foreign key in database. That's what we want.
Read more about Relation
...
@ManyToOne(() => RoleEntity, (role: RoleEntity) => role.id, {
createForeignKeyConstraints: false,
})
@JoinColumn({ name: 'role_id', referencedColumnName: 'id' })
role: Relation<RoleEntity>;
Entity:
$ npm run generate:entity
Note:
-
Input Entity name follow this format:
entity name
Ex: Inputuser data
=> outputuserData
-
Input Table name follow this format:
tablename
Ex: Inputuser_data
=> outputuser_data
-
Don't forget to index the column that you use to query data (or search/filter as you want). It will help you to query data faster.
Read more about Index
Repository:
$ npm run generate:repository
Note:
- Input Repository name follow this format:
repository name
Ex: inputuser data
=> outputuserData
Repository name should be the same as Entity name.
There are 2 ways to use Repository in this project.
Inject Repository into Service directly.
Inject Repository into DatabaseService then use it in Service. (Recommended)
In *.module.ts
file, add Repository into providers
array.
@Module({
providers: [
...
ExampleRepository,
],
...
})
In *.service.ts
file, inject Repository into Service.
constructor(
private readonly exampleRepository: ExampleRepository
) {}
...
async example() {
return this.userDataRepository.find();
}
In database.module.ts
file, add Repository into repositories
array, or add it into providers
array.
const repositories = [..., ExampleRepository];
In database.service.ts
file, inject Repository into DatabaseService.
constructor(
...
public readonly example: ExampleRepository,
) {}
...
In *.service.ts
file, inject DatabaseService into Service.
constructor(
private readonly database: DatabaseService
) {}
...
example() {
return this.database.example.find();
}
Migration is a way to create or update database schema. It will help you to create or update table, column, etc. To use this feature, you need to follow these steps:
-
Write the migration script in
migrations/init.ts
file. -
Run this command to run migration file:
$ npm run migration:run
- Run this command to revert migration file:
$ npm run migration:revert
You can also generate migration file using this command:
$ npm run migration:generate --name=<migration name>
After that, you can import the migration file into migrations/configs/db.config.ts
file and run migration command above.
Read more about TypeORM Migration
Seeding data is a way to insert data into database. It will help you to insert initial data. To use this feature, you need to follow these steps:
- Create factory file in
src/database/typeorm/factories
folder. - Create seed file in
src/database/typeorm/seeds
folder. - Then run this command to seed data into database:
$ npm run seed:run
There are some examples in src/database/typeorm/factories
and src/database/typeorm/seeds
folder.
Read more about Database Seeding
migrations
public- THIS FOLDER USE FOR UPLOAD FILE, DOWNLOAD FILE, EVERYTHING RELATED TO PUBLIC.
test
src
├── app.module.ts
├── bootstrap
├── common
├── config
├── database
│ ├── typeorm
│ │ ├── entities
│ │ ├── repositories
│ │ ├── factories
│ │ ├── seeds
│ │ ├── database.module.ts
│ │ ├── database.service.ts
│
├── modules
│ ├── example
│ │ ├── dto
│ │ ├── example.controller.ts
│ │ ├── example.module.ts
│ │ ├── example.service.ts
│
├── shared
│ ├── services
│
When you create a new module, you need to follow the structure above.
Use npm run generate:module
to generate module and it will create files follow the structure for you.
- Module folder:
moduleName
. Ex:user
,userData
- File name:
moduleName
.(controller|module|service|dto|entity|repository).ts. Ex:user.controller.ts
,userData.service.ts
- Class name:
ModuleName
+ (Controller|Module|Service|Dto|Entity|Repository). Ex:UserController
,UserDataService
- Column name:
column_name
. Ex:first_name
,last_name
- Property of entity:
propertyName
. Ex:firstName
,lastName
- Table name:
table_name
. Ex:user_data
,user_role
- Permission action:
moduleName:actionName
. Ex:user:create
,userData:findAll
Each API will have a permission. The permission of the API will determine what role can access the API.
Each role will have a list of permissions.
Each user has a role. The role of the user will determine what api the user can access.
Admin role will automatically have all permissions. You don't need to assign permissions to Admin role.
Ex: User role has a list of permissions: post:create
, post:findAll
, post:update
, post:delete
.
So, User role can create, find all, update, delete post.
First, insert @Permission
decorator into controller. Permission name will have format: moduleName:action
.
@Permission('post:findAll')
@Get()
async findAll() {
return this.postService.findAll();
}
Then run the app and it will create permission in database for you.
$ npm run start:dev
The name of permission will be Function name
+ Controller name
.
Ex: Find all Post
.
You can see all permissions in permissions
table.
Finally, you can assign permission to role in database.
INSERT INTO `roles_permissions` (`role_id`, `permission_id`) VALUES (1, 1);
If you want to bypass RBAC, you can pass BYPASS_PERMISSION
into @Permission
decorator. BYPASS_PERMISSION
is imported from '~/common/constants/constant'
.
@Permission(BYPASS_PERMISSION)
@Get()
async findAll() {
return this.postService.findAll();
}
IMPORTANT: You have to use @Permission
decorator in every API. If you don't use @Permission
decorator, the API will be blocked by default.
To upload media file, you can use /media/upload
api with multipart/form-data
content type.
Go to Swagger API Documentation: http://localhost:8080/docs/#/Media
Note: We have interceptors to format the response. So, you don't need to format the response manually. This is just for your information.
{
"result": true,
"message": "Success",
"data": {
"id": 1,
"name": "John Doe",
"email": "johndoe@email.com"
}
}
Or
{
"result": true,
"message": "Success",
"data": [
{
"id": 1,
"name": "John Doe",
"email": "johndoe@email.com"
}
],
"pagination": {
"page": 1,
"perPage": 10,
"totalRecords": 100,
"totalPages": 10
}
}
Error code will be 400, 401, 403, 404, 500, etc.
And response will return to HTTP status code.
Read more about HTTP Status Code
{
"result": false,
"message": "Unauthorized",
"data": null,
"statusCode": 401
}
Or
{
"result": false,
"message": [
{
"field": "unitId",
"error": "Đơn vị vật tư không tồn tại"
},
{
"field": "categoryId",
"error": "Loại vật tư không tồn tại"
}
],
"data": null,
"statusCode": 400
}
Public folder is used for upload file, download file, everything related to public.
Every files in public folder will be ignored by git.
And you can access the file in public folder by this url: http://localhost:8080/public/<file name>
It will return the file to you.
DON'T PUT YOUR FILE IN src
FOLDER. PUT IT IN public
FOLDER.
AND DON'T PUT ANYTHING IMPORTANT IN PUBLIC FOLDER. EVERYONE CAN ACCESS IT.
-
main
for Production -
dev
for Development -
feat/*
for features (new features, new APIs, etc., check out fromdev
branch) -
fix/*
for bug fixes (fix bugs, fix errors, etc., check out fromdev
branch)Maybe we will have
stg
branch for staging environment in the future. (if we or clients have budget)
feat/*
and fix/*
will be created from dev
branch and will be merged into dev
branch.
dev
branch will be merged into main
branch.
Create pull request to merge feat/*
and fix/*
branches into dev
branch and dev
branch into main
branch.
We use Swagger for API Documentation.
To integrate Swagger into NestJS, read more HERE
Swagger API Documentation: http://localhost:8080/docs
Using Jest for API Testing.
Every Module will have a test file in test
folder.
# run all test
$ npm run test
# run test in watch mode
$ npm run test:watch
# run test with coverage
$ npm run test:cov