This is the backend services container. Data is stored in a Postgres database and served over HTTPS to the frontend (either at build time for things that can be server-rendered, otherwise at run time). Most services are part of a NestJS application. Prisma is the application's ORM. OpenAPI Swagger documentation is automatically generated by the server at http://localhost:3100/api/ in local development environments, or in any other environment by adding /api
to the api URL. This can be helpful to get a more visual overview of all available endpoints.
The following commands are for macOS / Linux, but you can find equivalent instructions for Windows machines online.
Configuration of the backend is pulled from environment variables defined in an .env
file in the api directory. Copy the .env.template
file in api into a .env
file. Some keys are secret and are internally available. The template file includes default values and descriptions of each variable.
If you don't have yarn installed, you can install homebrew with these instructions and then do so with brew install yarn
.
We are currently using Node version 18. You can install Node using homebrew with the following command: brew install node@18
.
If you have multiple versions of Node installed, you can use nvm (node version manager), or other similar tools, to switch between them. Ensure you're on the right version by checking with node -v
.
If along the way you get env: node: No such file or directory
, inspect the output from installing node for instructions on if you might need to add node to certain terminal paths.
You can install Postgres using homebrew with the following command: brew install postgresql@15
. You then start it with brew services start postgresql@15
.
Install project dependencies with yarn install
from within the api directory.
The following command will generate and build the Prisma schema and setup the database with seeded data: yarn setup:dev
.
If you would prefer to have it setup with more realistic data you can instead run: yarn setup
.
If this is your first time running this command and you see psql: error: FATAL: database "<username>" does not exist
you may need to run createdb <username>
first.
You will also need to update the DATABASE_URL
environment variable to include your username.
If you're using VSCode, you can install the Postgres explorer extension to inspect your local database. When you click on the + to create a new connection, you can use the following inputs to each question to create a connection to the newly created database: localhost
, <username>
, hit enter for password, 5432
, standard, bloom_prisma
, and a descriptive name like local-bloom
. Once the connection is established, you can inspect the database.
To start the application run: yarn dev
.
If you're using VSCode, you can install the Prisma extension to add syntax highlighting and formatting to Prisma schema files.
To modify the Prisma schema you will need to work with the schema.prisma file. This file controls the following:
- The Structure of each model (entity if you are more familiar with TypeORM)
- The Relationships between models
- Enum creation for use in both the API and the database
- How Prisma connects to the database
You will need to:
- Add the field in the DTO
- Run
yarn generate:client
to add the type to the swagger file - Manually add the field to
schema.prisma
- Run
yarn prisma migrate dev --name <name of migration>
to create the migration file
We use the following conventions:
- model and enum names are capitalized camel case (e.g. HelloWorld)
- model and enum names are @@map()ed to lowercase snake case (e.g. hello_world)
- a model's fields are lowercase camel case (e.g. helloWorld)
- a model's fields are @map()ed to lowercase snake case (e.g. hello_world)
Backend endpoints live in controllers under src/controllers
. They follow the NestJs standards
Controllers are given the extension .controller.ts
and the model name (listing, application, etc) is singular. So for example listing.controller.ts
.
The exported class should be in capitalized camel case (e.g. ListingController
).
DTOs (Data Transfer Objects) are how we flag what fields endpoints will take in, and what the form of the response from the backend will be.
We use the packages class-transformer & class-validator for this.
DTOs are stored under src/dtos
, and are broken up by what model they are related to. There are also shared DTOs which are stored under the shared sub-directory.
DTOs are given the extension .dto.ts
and the file name is lowercase kebab case (e.g. listings-filter-params.dto.ts
).
The exported class should be in capitalized camel case (e.g. ListingFilterParams
) and does not include the DTO as a suffix.
These are enums used by NestJs primarily for either taking in a request or sending out a response. Database enums (enums from Prisma) are part of the Prisma schema and are not housed here.
They are housed under src/enums
and the file name is lowercase kebab case and end with -enum.ts
.
So for example filter-key-enum.ts
.
The exported enum should be in capitalized camel case (e.g. ListingFilterKeys
).
Modules connect the controllers to services and follow NestJS standards.
Modules are housed under src/modules
and are given the extension .module.ts
. The model name (listing, application, etc) is singular. So for example listing.module.ts
.
The exported class should be in capitalized camel case (e.g. ListingModule
).
Services are where business logic is performed as well as interfacing with the database.
Controllers should be calling functions in services in order to do their work.
The follow the NestJS standards.
Services are housed under src/services
and are given the extension .services.ts
. The model name (listing, application, etc) is singular. So for example listing.service.ts
.
The exported class should be in capitalized camel case (e.g. ListingService
).
We currently use guards for 2 purposes. Passport guards and Permissioning guards.
Passport guards (jwt.guard.ts, mfa.guard.ts, and optional.guard.ts) verify that the request is from a legitimate user. JwtAuthGuard does this by verifying the incoming jwt token (off the request's cookies) matches a user. MfaAuthGuard does this by verifying the incoming login information (email, password, mfaCode) matches a user's information. OptionalAuthGuard is used to allow requests from users not logged in through. It will still verify the user through the JwtAuthGuard if a user was logged in.
Passport guards are paired with a passport strategy (jwt.strategy.ts, and mfa.strategy.ts), this is where the code to actually verify the requester lives.
Hopefully that makes sense, if not think of guards as customs agents, and the passport strategy is what the guards look for in a request to allow entry to a requester. Allowing them access the endpoint that the guard protects.
NestJS passport docs NestJS guards docs
Permissioning guards (permission.guard.ts, and user-profile-permission-guard.ts) verify that the requester has access to the resource and action they are trying to perform. For example a user that is not logged in (anonymous user) can submit applications, but cannot create listings. We leverage Casbin to do user verification.
There are 2 different kinds of tests that the backend supports: Integration tests and Unit tests.
Integration Tests are tests that DO interface with the database, reading/writing/updating/deleting data from that database.
Unit Tests are tests that MOCK interaction with the database, or test functionality directly that does not interact with the database.
Integration Tests are housed under test/integration
, and files are given the extension .e2e-spec.ts
.
These tests will generally test going through the controller's endpoints and will mock as little as possible. When testing the database should start off as empty and should be reset to empty once tests are completed (i.e. data is cleaned up).
Running the following will run all integration tests yarn test:e2e
.
Unit Tests are housed under test/unit
, and files are given the extension .spec.ts
.
These tests will generally test the functions of a service, or helper functions. These tests will mock Prisma and therefore will not interface directly with the database. This allows for verifying the correct business logic is performed without having to set up the database.
Running the following will run all unit tests: yarn test
We have set up both code coverage and code coverage benchmarks. These benchmarks must be met for your PR to pass CI checks. Test coverage is calculated against both the integration and unit test runs. You can run test coverage with the following: yarn test:cov