A boilerplate for NestJS, using Fastify.
- Config
- Database
- Swagger (API docs)
- Query Parsing
- File Upload
- Logging
- Request Context
- Auth guard
- Rate limiting
- Request body validation
- Exception Handling
- OpenTelemetry
- Docker
- Testing
- Continuous Integration
$ npm install
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
The node-config package to manage configs.
Default config values are found in default.json.
These values can be overridden by:
- Creating config files as described in node-config docs
- Creating a
local.json
file in config/ - Creating a
.env
file in the project directory. (supported via dotenv) - Setting environment variables. See the environment variable mappings in custom-environment-variables.json.
CORS is configurable via the config.
The following values are acceptable for config.cors.origins
:
*
- Will accept any origin. Theaccess-control-allow-origin
header will always respond with the exact origin of the request, no a*
.https://example.com,https://example2.com
- Will accept all domains in the comma separated list.
Objection (which uses Knex.js) is used for database operations.
It uses PostgreSQL by default, but that can be changed by changing the client
in knexfile.ts.
See Knex documentation for supported databases.
Conversion of column names from camel case to snake case is automatically done in most cases.
It however does not work in all cases. See excerpt below from Objection docs
(Snake case to camel case conversion):
When the conversion is done on objection level only database columns of the returned rows (model instances) are convered to camel case. You still need to use snake case in relationMappings and queries. Note that insert, patch, update and their variants still take objects in camel case. The reasoning is that objects passed to those methods usually come from the client that also uses camel case.
Schema changes require migrations.
Migrations can be created using:
npm run migration:make
When the server starts, migrations will run automatically, or, you can run the migrations
using npm run migration:latest
Swagger documentation is automatically generated from the routes.
By default it is available at http://127.0.0.1:3000/docs
URL query to DB query parsing is available. See query-parser.ts.
Note: There is currently no limitation put on the complexity of the query, so this should be exposed with caution.
Example:
limit=10&page=1&orderBy=(averageRating,DESC)&filter=(authors,=,Bill Bryson):(yearPublished,>,2000)
SELECT "*"
FROM "..."
WHERE "authors" = 'Bill Bryson'
AND "year_published" > '2000'
ORDER BY "average_rating" DESC
LIMIT 10
Paging can either be done using limit
and page
.
Filters take the format of (column,operand,value)
where the operand can be =,>,>=,<,<=
.
Column names are automatically converted to snake case.
Multiple filters can be specified using a colon :
as the delimiter.
Ordering takes the format of (column,direction)
where direction can be desc,DESC,asc,ASC
.
Multiple orderings can be specified using a colon :
as the delimiter.
File uploads are available.
maxSize: number
- Max size in bytes. Default 5MB.
uploadDir: string
- Upload directory. If blank, it will default to the OS's temp directory.
removeAfterUpload: string
- Whether to delete files after the request completes.
Upload a single file
@ApiProperty({ type: "string", format: "binary" })
@IsNotEmpty()
@IsObject({ message: "$property must be a single file" })
@ValidateNested({ message: "$property must be a file" })
@Type(() => UploadedFileDto)
file2: UploadedFileDto;
Upload multiple files
@ApiProperty({ type: "array", items: { type: "string", format: "binary" } })
@IsNotEmpty()
@IsArray({ message: "$property must be multiple files" })
@ValidateNested({ message: "$property must be a file", each: true })
@Type(() => UploadedFileDto)
file1: UploadedFileDto[];
See FileUploadDto for a full example.
Winston is used as a logging library.
Create a logger instance using new Logger()
.
A parameter can be passed into the constructor and to be used as a tag (defaults to "Application").
For example,
import { Logger } from "@/logging/Logger";
const logger = new Logger("AppService");
this.logger.debug("Hello!");
// 2019-05-10 19:47:21.570 | debug: [AppService] Hello!
A custom tag can be passed into the log functions.
this.logger.debug("Hello!", { tag: "AppService.getHello" });
// 2019-05-10 19:54:43.062 | debug: [AppService.getHello] Hello!
Extra data can be passed into the log functions. To enable printing it to the console, set the
config logging.logDataConsole
to true
.
this.logger.debug(`Hello ${ctx.traceId}`, { data: { traceId: ctx.traceId } });
// 2025-01-15 15:11:57.090 | debug: [AppService.getHello] Hello 47e4a86ea7c0676916b45bed6c80d1bb
// {
// "traceId": "47e4a86ea7c0676916b45bed6c80d1bb"
// }
To log to other locations, a custom transport is needed. See SampleTransport for an example.
Private keys are automatically redacted in logged data.
The private keys are specified in redact.ts
{
"username": "mark",
"contact": {
"email": "REDACTED"
}
}
Request context. be accessed using the @ReqCtx()
header.
It contains a traceId
.
@Get()
function getHello(@ReqCtx() ctx: IReqCtx) {
console.log(ctx.traceId) // 0d8df9931b05fbcd2262bc696a1410a6
}
An authentication guard is available in auth.guard.ts
It can be enabled by adding a UseGuards
decorator to a controller or route
@UseGuards(AuthGuard)
or globally
app.useGlobalGuards(new AuthGuard());
A rate limiter is configured
using @nestjs/throttler.
It defaults to 100 request per minute per IP (configurable in default.json)
.
class-validator and class-transformer are used to validate request bodies.
See class-validator decorators for available validation options.
An example of a response to an invalid body:
{
"status": 400,
"message": "Validation errors with properties [name,username,contact.mail,contact.email]",
"code": "ValidationError",
"meta": [
{
"property": "name",
"constraints": [
"property name should not exist"
]
},
{
"property": "username",
"constraints": [
"username should not be empty"
]
},
{
"property": "contact.mail",
"constraints": [
"property mail should not exist"
]
},
{
"property": "contact.email",
"constraints": [
"email should not be empty"
]
}
]
}
NB: Raising an error when unknown values are passed can be disabled by
setting validator.forbidUnknown
to false
in the config.
Cleaning response objects using can be enabled using the @Serialize(ClassName)
decorator.
It uses class-transformer.
Exceptions should be thrown using the custom HttpException class.
throw new HttpException(404, `User ${1} was not found`, ErrorCodes.INVALID_USER, { id: 1 });
{
"status": 404,
"message": "User 1 was not found",
"code": "InvalidUser",
"traceId": "775523bae019485d",
"meta": {
"id": 1
}
}
Regular errors and unhandled exceptions are also caught and returned as a 500 response.
{
"status": 500,
"message": "Uncaught exception",
"code": "InternalError",
"traceId": "d3cb1b2b3388e3b1"
}
OpenTelemetry support in included with support for traces, metrics and logs.
@opentelemetry/auto-instrumentations-node is set up to automatically collect metrics and spans for various services
See the observability README for a compose file with various services for collecting and viewing signals.
Note: Instrumentation needs to be enabled via config
Automatic instrumentation is enabled and will suite most needs.
Custom spans can be created as described in the OpenTelemetry docs.
HTTP metrics are automatically collected by @opentelemetry/instrumentation-http
sum by(http_route) (rate(nb_http_server_duration_milliseconds_count[1m]))
See OpenTelemetry docs for how to create custom metrics.
const meter = opentelemetry.metrics.getMeter("UserService");
const getUserCounter = this.meter.createCounter("get_user")
getUserCounter.add(1, { user_id: id });
Logs are sent to the OpenTelemetry Collector using the OtelTransport.
See logging for how to log.
The application can be run using docker.
Build
docker build -t nest-boilerplate .
docker run -p 3000:3000 nest-boilerplate
Docker Compose can be used to start the application and a database.
docker compose up -d
There exist unit tests for controllers and services.
Dependencies are mocked using jest
.
npm run test
A Docker container is created to run end to end tests.
# e2e tests (docker)
npm run test:e2e
# e2e test (locally)
npm run test:e2e:local
Load tests are written using Grafana k6.
See load-test/ directory.