Based on Apollo Server (GitHub).
And, what you see in package.json.
yarn add @volcanicminds/backend
yarn upgrade-deps
NODE_ENV=development
HOST=0.0.0.0
PORT=2230
JWT_SECRET=yourSecret
JWT_EXPIRES_IN=5d
JWT_REFRESH=true
JWT_REFRESH_SECRET=yourRefreshSecret
JWT_REFRESH_EXPIRES_IN=180d
# LOG_LEVEL: trace, debug, info, warn, error, fatal
LOG_LEVEL=info
LOG_COLORIZE=true
LOG_TIMESTAMP=true
LOG_TIMESTAMP_READABLE=true
LOG_FASTIFY=false
GRAPHQL=false
SWAGGER=true
SWAGGER_HOST=myawesome.backend.com
SWAGGER_TITLE=API Documentation
SWAGGER_DESCRIPTION=List of available APIs and schemas to use
SWAGGER_VERSION=0.1.0
For docker may be useful set HOST as 0.0.0.0 (instead 127.0.0.1).
yarn dev
yarn start
yarn prod
When you execute yarn dev
the server is restarted whenever a .js/.ts file is changed (thanks to nodemon)
yarn test
yarn test -t 'Logging'
Refer to jest for more options.
In the .env file you can change log settings in this way:
# LOG_LEVEL: trace, debug, info, warn, error, fatal
LOG_LEVEL=debug
LOG_TIMESTAMP=true
LOG_TIMESTAMP_READABLE=false
LOG_COLORIZE=true
Log levels:
a bit of code:
log.trace('Annoying message')
log.debug('Where is my bug?')
log.info('Useful information')
log.warn(`Hey pay attention: ${message}`)
log.error(`Catch an exception: ${message}`)
log.fatal(`Catch an exception: ${message} even if it's too late, sorry.`)
// use the proper flag to check if the level log is active (to minimize phantom loads)
log.i && log.info('Total commissions -> %d', aHugeCalculation())
// f.e.
log.t && log.trace('print a message')
log.d && log.debug('print a message')
log.i && log.info('print a message')
log.w && log.warn('print a message')
log.e && log.error('print a message')
log.f && log.fatal('print a message')
Other settings:
Defaults, see logger.ts:
const logColorize = yn(LOG_COLORIZE, true)
const logTimestamp = yn(LOG_TIMESTAMP, true)
const logTimestampReadable = yn(LOG_TIMESTAMP_READABLE, true)
JWT_SECRET=yourSecret
JWT_EXPIRES_IN=5d
JWT_REFRESH=true
JWT_REFRESH_SECRET=yourRefreshSecret
JWT_REFRESH_EXPIRES_IN=180d
With reply.jwtSign(payload)
is possible obtain a fresh JWT token. Each authenticated calls must be recalled specifying in the header:
Authorization: Bearer <generated-token>
With await reply.server.jwt['refreshToken'].sign(payload)
is possible obtain a new Refresh JWT token.
All tokens (authorization and refresh) can be invalidated through the appropriate route.
Example: Both JWT_SECRET
and JWT_REFRESH_SECRET
can be generated with a command like openssl rand -base64 64
In the .env file you can change swagger settings in this way:
SWAGGER=true
SWAGGER_HOST=http://localhost:2230
SWAGGER_TITLE=Volcanic API Documentation
SWAGGER_DESCRIPTION=List of available APIs and schemes to use
SWAGGER_VERSION=0.1.0
SWAGGER_PREFIX_URL=/documentation
Under the folder src/config
is possible add a file plugin.ts
where you can activate/customize some modules in this way:
// src/config/plugins.ts
module.exports = [
{
name: 'cors',
enable: false,
options: {}
},
{
name: 'rateLimit',
enable: false,
options: {}
},
{
name: 'helmet',
enable: false,
options: {}
},
{
name: 'compress',
enable: false,
options: {}
}
]
Minimal setup (routes.ts):
module.exports = {
routes: [
{
method: 'GET',
path: '/',
handler: 'myController.test'
}
]
}
Some notes:
// src/api/example/routes.ts
module.exports = {
config: {
title: 'Example of routes.ts',
description: 'Example of routes.ts',
controller: 'controller',
tags: ['user', 'code'], // swagger
enable: true,
deprecated: false, // swagger
version: false // swagger
},
routes: [
{
method: 'GET',
path: '/',
roles: [],
handler: 'demo.user',
middlewares: ['global.isAuthenticated'],
config: {
enable: true,
title: 'Demo title', // swagger summary
description: 'Demo description', // swagger
tags: ['user', 'code'], // swagger
deprecated: false, // swagger
version: false, // swagger
response: {
200: {
description: 'Successful response',
type: 'object',
properties: {
id: { type: 'number' }
}
}
} // swagger
}
}
]
}
// src/api/example/controller/demo.ts
import { FastifyReply, FastifyRequest } from '@volcanicminds/backend'
export function user(req: FastifyRequest, reply: FastifyReply) {
reply.send(req.user || {})
}
Useful methods / objects:
req.user
to grab user data (validated and linked by JWT).req.data()
to grab query or body parameters.req.parameters()
to grab params data.req.roles()
to grab Roles (as string[]
) from req.user
if compiled.req.hasRole(role:Role)
to check if the Role is appliable for req.user
.By default, there are some basic roles:
In this way you can add custom roles:
// src/config/roles.ts
import { Role } from '@volcanicminds/backend'
export const roles: Role[] = [
{
code: 'customer',
name: 'Customer',
description: 'Customer role'
}
]
You can use something like this to specify which roles (routes.ts) can recall some routes:
roles: [roles.admin, roles.public]
Use package @volcaniminds/typeorm
(npm)
yarn add @volcanicminds/typeorm
It’s possible add hook to application or request/reply lifecycles. More info on Fastify Hooks.
Available hooks are:
const hooks = [
'onRequest',
'onError',
'onSend',
'onResponse',
'onTimeout',
'onReady',
'onClose',
'onRoute',
'onRegistry',
'preParsing',
'preValidation',
'preSeralization',
'preHandler'
]
Under src
create the hooks
folder and inside add the hook as shown in the fastify docs, for example:
// src/hooks/onRequest.ts
async function hook(req, reply) {
log.debug('onRequest called')
}
export { hook }
It’s possible add schemas referenceable by $ref
. More info on Fastify Validation & Serialization.
Under src
create the schemas
folder and inside add the schema as shown in the fastify docs, for example:
// src/schemas/commonSchemas.ts
export const commonSchema = {
$id: 'commonSchema',
type: 'object',
properties: {
hello: { type: 'string' }
}
}
export const commonSchemaAlt = {
$id: 'commonSchemaAlt',
type: 'object',
properties: {
world: { type: 'string' }
}
}
So, in your routes.ts
(under the section config
) you’ll can use something like this:
params: { $ref: 'commonSchema#' },
query: { $ref: 'commonSchema#' },
body: { $ref: 'commonSchema#' },
headers: { $ref: 'commonSchema#' }
It’s possible to specify that all JWT tokens belonging to the user who logs in are reset at each login. To enable this feature, it’s necessary to add or change the property reset_external_id_on_login
to true
(the default is false
).
// src/config/general.ts
'use strict'
module.exports = {
name: 'general',
enable: true,
options: {
reset_external_id_on_login: true
}
}
It’s possible to add a job scheduler. For more information, go to Fastify Schedule. To enable this feature, it’s necessary to add or change the property scheduler
to true
(the default is false
).
// src/config/general.ts
'use strict'
module.exports = {
name: 'general',
enable: true,
options: {
scheduler: true
}
}
All jobs are to be created and placed in appropriate files under the /src/schedules/ folder. Each file name must follow the pattern *.job.ts (for example, test.job.ts).
Inside each job, both the configuration part and the job to be executed must be included using this syntax:
// src/schedules/test.job.ts
import { JobSchedule } from '@volcanicminds/backend'
export const schedule: JobSchedule = {
active: true,
interval: {
seconds: 2
}
}
export async function job() {
log.info('tick job 2 every 2 seconds')
}
The job scheduling can have this configuration:
export interface JobSchedule {
active: boolean // boolean (required)
type?: string // cron|interval, default: interval
async?: boolean // boolean, default: true
preventOverrun?: boolean // boolean, default: true
cron?: {
expression?: string // required if type = 'cron', use cron syntax (if not specified, cron will be disabled)
timezone?: string // optional, like "Europe/Rome" (to test)
}
interval?: {
days?: number // number, default 0
hours?: number // number, default 0
minutes?: number // number, default 0
seconds?: number // number, default 0
milliseconds?: number // number, default 0
runImmediately?: boolean // boolean, default: false
}
}
The active property is a boolean and is mandatory.
The type
property can have values of cron
or interval
(default).
If the type is cron, the properties defined under cron
are also considered.
If the type is interval, the properties defined under interval
are also considered.
For cron type, the cron.expression
property is mandatory and indicates the scheduling to be executed.
The timezone
property is considered experimental and should be defined, for example, as "Europe/Rome"
.
Below an example:
// src/schedules/test.job.ts
import { JobSchedule } from '@volcanicminds/backend'
export const schedule: JobSchedule = {
active: true,
type: 'cron',
// Run a task every 2 seconds
cron: {
expression: '*/2 * * * * *'
}
}
Below the cron schema:
┌──────────────── (optional) second (0 - 59)
│ ┌────────────── minute (0 - 59)
│ │ ┌──────────── hour (0 - 23)
│ │ │ ┌────────── day of month (1 - 31)
│ │ │ │ ┌──────── month (1 - 12, January - December)
│ │ │ │ │ ┌────── day of week (0 - 6, Sunday-Monday, Sunday is equal 0 or 7)
│ │ │ │ │ │
│ │ │ │ │ │
* * * * * *
// f.e. for every 2 seconds
const expression = '*/2 * * * * *'
A useful site that can be used to check a cron configuration is crontab.guru
For interval type, the sum of the properties days, hours, minutes, seconds, milliseconds
(properly converted) must be equal to or greater than 1 second, otherwise the job will not be executed.
The runImmediately
property indicates that the interval task will be executed for the first time immediately and not after the defined wait time.
Below an example:
// src/schedules/test.job.ts
import { JobSchedule } from '@volcanicminds/backend'
export const schedule: JobSchedule = {
active: true,
type: 'interval',
// Run a task every 1h 5m 30s
interval: {
days: 0,
hours: 1,
minutes: 5,
seconds: 30,
milliseconds: 0,
runImmediately: false
}
}
Other properties common to both types of jobs are: