A DDD application focused on separation of concerns and scalability

Firebase NodeJS Firestore API

A DDD application focused on separation of concerns and scalability.
Loosely coupling with clear dependency graphs provided by Inversion of Control.

Getting Started

Project architecture

.
└── src
    ├── api # Layer that exposes application to external world (primary adapters)
    │   └── http # Exposes application over HTTP protocol
    ├── app # Layer that composes application use cases
    ├── domain # Business domain classes and everything that composes domain model
    ├── infra # Communication with what is external of application
    └── dist # Common functionalities

Setup Development Environment

Set firebase project alias

firebase use develop

Note: You have install GPG Suit if it hasn’t been installed on your PC yet. Learn more.

Set environment configuration

yarn functions:set-env

Retrieve current environment configuration

yarn functions:export-env

Start firestore for database (emulators)

yarn firestore

Start the dev server

Open another terminal to start the development server:

yarn dev

API Documentation

Using APIDoc as documentation for Restful APIs.

Running the following command to generate documentation:

yarn docs

VS Code snippet

Install the following extenstion to generate APIDoc format

https://marketplace.visualstudio.com/items?itemName=myax.appidocsnippets

Using API

Code Generator

Running the following command and follow the instruction to generate a new API:

node generate.js

Authorization

To set the custom claims for user, please use the sample code below:

@inject(TYPES.AuthService) private readonly _authService: AuthService;

// ...

// Firebase user uid = I4bwuWF6uRUe10wqil7DrxvSdvm2
// Role is admin | customer
this._authService.setCustomUserClaims('I4bwuWF6uRUe10wqil7DrxvSdvm2', {role: 'admin'});

Use authorization middleware decorator in controller:

@httpGet('/', authorize({ roles: ['admin']}))
public async getUsers(@response() res: Response): Promise<User[] | void> {
    try {
        const data = await this.userService.getUsers();
        // The AuthProvider decoded user token after logging.
        // You can see the decodedIdToken and user information below:
        // console.log('Decoded Id Token: ', this.httpContext.user.details);

        return data;
    } catch (error) {
        res.status(HttpStatus.BAD_REQUEST).json({ error: error.message });
    }
}

If you want to allow the action for some roles, please add more items in the roles array, see it below

@httpGet('/', authorize({ roles: ['admin', 'customer']}))

If the action is required two role for executing, please add two middleware decorators to @httpGet decorator:

@httpGet('/', authorize({ roles: ['admin']}), authorize({ roles: ['customer']}))

Note: We have some APIs that open to publish for all users. It means without authentication and authorization.

Manual check user authenticated in controller:

@httpGet('/')
public async fooAction() {
    const isAuthenticated: boolean = this.httpContext.user.isAuthenticated();
    // ...
}

Create an user

curl -XPOST -i -H"Content-Type: application/json" -d'{"uid" : 1, "email": "abc@gmail.com", "name": "Learning Kubernetes"}' http://localhost:5001/firebase-project-name/asia-east2/api/v1/users

# output
bC4XfImu5r9SC3UfhCFr

List items

curl -I http://localhost:5001/firebase-project-name/asia-east2/api/v1/users | jq

Sample response data

[
    {
        "_id": "zcCqsJbdCVjbvf8Inqzd",
        "props": {
            "name": "Jude Nguyen",
            "email": "jude.stdio@gmail.com"
        }
    }
]

List item by id

curl -I http://localhost:5001/firebase-project-name/asia-east2/api/v1/users/bC4XfImu5r9SC3UfhCFr | jq

Sample response data

{
    "_id": "zcCqsJbdCVjbvf8Inqzd",
    "props": {
        "name": "Jude Nguyen",
        "email": "jude.stdio@gmail.com"
    }
}

Update User

curl -XPUT -i -H"Content-Type: application/json" -d'{"uid" : 1, "email": "abc@gmail.com", "name": "Learning React"}' http://localhost:5001/firebase-project-name/asia-east2/api/v1/users/bC4XfImu5r9SC3UfhCFr

Delete User

curl -XDELETE -i http://localhost:5001/firebase-project-name/asia-east2/api/v1/users/qPXJqaJrcly2BXja1v8v

Migration & Seeding

The database seeding provides an easy way to generate sample data in the database using seed classes.

// File: src/infra/database/migration/seeding/user.ts

@provide(TYPES.UserSeeding)
class UserSeeding implements ISeeding {
    constructor(
        @inject(TYPES.UserRepository)
        private readonly userRepository: IUserRepository
    ) {}

    async run() {
        // ...
    }
}

How to use it in CLI:

  1. Open file src/cli/seeding.ts then added the new seeding class.

type ISeedingType = 'UserSeeding';

class Seeding {
    #seedings: ISeedingType[] = ['UserSeeding'];
}
  1. Build & Run seedings

Before run seedings using CLI, you must use firebase function shell to run user seeding first.

$ yarn shell

> userSeeding({})

Run other seedings

$ cd /path/to/project
$ yarn build && chmod +x ./dist/cli/index.js
$ yarn tools
# Run single seeding
$ ./dist/cli/index.js --seed UserSeeding
$ # Run all seeding with
$ ./dist/cli/index.js --seedall

VS Code

Automatically fix code in VS Code

{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
}

Troubleshooting

GitHub

View Github