π Graceful Server π’
Tiny (~5k), KISS, dependency-free Node.JS library to make your Rest API graceful.
- Features
- Requirements
- Installation
- Endpoint
- Example
- API
- Integration with Docker
- Integration with Kubernetes
- Thanks
- Sponsors
- Donate
Features
β It's listening system events to gracefully close your API on interruption.
β It facilitates the disconnect of data sources on shutdown.
β It facilitates the use of liveness and readiness.
β It manages the connections of your API.
β It avoid boilerplate codes.
β Kubernetes compliant.
β Dependency-free.
β KISS code base.
Requirements
β NodeJS >= 14.0
Installation
NPM
npm install --save @gquittet/graceful-server
Yarn
yarn add @gquittet/graceful-server
PNPM
pnpm add @gquittet/graceful-server
Endpoint
Below you can find the default endpoint but you can setup or disable them. To do that, check out the Options part.
/live
The endpoint responds:
200
status code with the uptime of the server in second.
{ "uptime": 42 }
Used to configure liveness probe.
/ready
The endpoint responds:
200
status code if the server is ready.
{ "status": "ready" }
503
status code with an empty response if the server is not ready (started, shutting down, etc).
Example
ExpressJS
The library works with the default HTTP NodeJS object. So, when you're using Express you can't pass
directly the app
object from Express. But, you can easily generate an HTTP NodeJS object from the app
object.
Just follow the bottom example:
const express = require('express')
const helmet = require('helmet')
const http = require('http')
const GracefulServer = require('@gquittet/graceful-server')
const { connectToDb, closeDbConnection } = require('./db')
const app = express()
const server = http.createServer(app)
const gracefulServer = GracefulServer(server, { closePromises: [closeDbConnection] })
app.use(helmet())
app.get('/test', (_, res) => {
return res.send({ uptime: process.uptime() | 0 })
})
gracefulServer.on(GracefulServer.READY, () => {
console.log('Server is ready')
})
gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => {
console.log('Server is shutting down')
})
gracefulServer.on(GracefulServer.SHUTDOWN, error => {
console.log('Server is down because of', error.message)
})
server.listen(8080, async () => {
await connectToDb()
gracefulServer.setReady()
})
As you can see, we're using the app
object from Express to set up the endpoints and middleware.
But it can't listen (you can do it but app
hasn't any liveness or readiness). The listening
of HTTP calls need to be done by the default NodeJS HTTP object (aka server).
Fastify
const fastify = require('fastify')({ logger: true })
const GracefulServer = require('@gquittet/graceful-server')
const gracefulServer = GracefulServer(fastify.server)
gracefulServer.on(GracefulServer.READY, () => {
console.log('Server is ready')
})
gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => {
console.log('Server is shutting down')
})
gracefulServer.on(GracefulServer.SHUTDOWN, error => {
console.log('Server is down because of', error.message)
})
// Declare a route
fastify.get('/', async (request, reply) => {
return { hello: 'world' }
})
// Run the server!
const start = async () => {
try {
await fastify.listen({ port: 3000 })
fastify.log.info(`server listening on ${fastify.server.address().port}`)
gracefulServer.setReady()
} catch (err) {
fastify.log.error(err)
process.exit(1)
}
}
start()
Be careful, if you are using Fastify v4.x.x with Node 16 and below, you have to use
await fastify.listen({ port: 3000, host: '0.0.0.0' })
because Node 16 and below does not support multiple addresses binding.
See: fastify/fastify#3536
Koa
const GracefulServer = require('@gquittet/graceful-server')
const Koa = require('koa')
const http = require('http')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
const server = http.createServer(app.callback())
gracefulServer = GracefulServer(server)
router.get('/test')
app.use(router.routes())
// response
app.use(ctx => {
ctx.body = 'Hello Koa'
})
gracefulServer.on(GracefulServer.READY, () => {
console.log('Server is ready')
})
gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => {
console.log('Server is shutting down')
})
gracefulServer.on(GracefulServer.SHUTDOWN, error => {
console.log('Server is down because of', error.message)
})
server.listen(8080, async () => {
gracefulServer.setReady()
})
As you can see, we're using the app
object from Express to set up the endpoints and middleware.
But it can't listen (you can do it but app
hasn't any liveness or readiness). The listening
of HTTP calls need to be done by the default NodeJS HTTP object (aka server).
HTTP Server
import http from 'http'
import url from 'url'
import GracefulServer from '@gquittet/graceful-server'
import { connectToDb, closeDbConnection } from './db'
const server = http.createServer((req, res) => {
if (req.url === '/test' && req.method === 'GET') {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
return res.end(JSON.stringify({ uptime: process.uptime() | 0 }))
}
res.statusCode = 404
return res.end()
})
const gracefulServer = GracefulServer(server, { closePromises: [closeDbConnection] })
gracefulServer.on(GracefulServer.READY, () => {
console.log('Server is ready')
})
gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => {
console.log('Server is shutting down')
})
gracefulServer.on(GracefulServer.SHUTDOWN, error => {
console.log('Server is down because of', error.message)
})
server.listen(8080, async () => {
await connectToDb()
gracefulServer.setReady()
})
API
GracefulServer
;((server: http.Server, options?: IGracefulServerOptions | undefined) => IGracefulServer) & typeof State
where State
is an enum that contains, STARTING
, READY
, SHUTTING_DOWN
and SHUTDOWN
.
IGracefulServerOptions
All of the below options are optional.
Name | Type | Default | Description |
---|---|---|---|
closePromises | (() => Promise)[] | [] | The functions to run when the API is stopping |
timeout | number | 1000 | The time in milliseconds to wait before shutting down the server |
healthCheck | boolean | true | Enable/Disable the default endpoints (liveness and readiness) |
kubernetes | boolean | false | Enable/Disable the kubernetes mode |
livenessEndpoint | string | /live | The liveness endpoint |
readinessEndpoint | string | /ready | The readiness endpoint |
If you use Kubernetes, enable the kubernetes mode to let it handles the incoming traffic of your application.
The Kubernetes mode will only work if you haven't disabled the health checks.
GracefulServer Instance
export default interface IGracefulServer {
isReady: () => boolean
setReady: () => void
on: (name: string, callback: (...args: any[]) => void) => EventEmitter
}
Integration with Docker
HEALTH CHECK in Dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD ["node healthcheck.js"]
Content of healthcheck.js
const http = require('http')
const options = {
timeout: 2000,
host: 'localhost',
port: 8080,
path: '/live'
}
const request = http.request(options, res => {
console.info('STATUS:', res.statusCode)
process.exitCode = res.statusCode === 200 ? 0 : 1
process.exit()
})
request.on('error', err => {
console.error('ERROR', err)
process.exit(1)
})
request.end()
Example of Dockerfile
POC level
FROM node:12-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD ["node healthcheck.js"]
CMD [ "node", "server.js" ]
Company level
FROM node:12-slim as base
ENV NODE_ENV=production
ENV TINI_VERSION=v0.18.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini && \
mkdir -p /node_app/app && \
chown -R node:node /node_app
WORKDIR /node_app
USER node
COPY --chown=node:node package.json package-lock*.json ./
RUN npm ci && \
npm cache clean --force
WORKDIR /node_app/app
FROM base as source
COPY --chown=node:node . .
FROM source as dev
ENV NODE_ENV=development
ENV PATH=/node_app/node_modules/.bin:$PATH
RUN npm install --only=development --prefix /node_app
CMD ["nodemon", "--inspect=0.0.0.0:9229"]
FROM source as test
ENV NODE_ENV=development
ENV PATH=/node_app/node_modules/.bin:$PATH
COPY --from=dev /node_app/node_modules /node_app/node_modules
RUN npm run lint
ENV NODE_ENV=test
RUN npm test
CMD ["npm", "test"]
FROM test as audit
RUN npm audit --audit-level critical
USER root
ADD https://get.aquasec.com/microscanner /
RUN chmod +x /microscanner && \
/microscanner your_token --continue-on-failure
FROM source as buildProd
ENV PATH=/node_app/node_modules/.bin:$PATH
COPY --from=dev /node_app/node_modules /node_app/node_modules
RUN npm run build
FROM source as prod
COPY --from=buildProd --chown=node:node /node_app/app/build ./build
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD ["node healthcheck.js"]
ENTRYPOINT ["/tini", "--"]
CMD ["node", "./build/src/main.js"]
Integration with Kubernetes
Don't forget to enable the kubernetes mode. Check here (related to this issue)
readinessProbe:
httpGet:
path: /ready
port: 8080
failureThreshold: 1
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /live
port: 8080
failureThreshold: 3
initialDelaySeconds: 10
# Allow sufficient amount of time (90 seconds = periodSeconds * failureThreshold)
# for the registered shutdown handlers to run to completion.
periodSeconds: 30
successThreshold: 1
# Setting a very low timeout value (e.g. 1 second) can cause false-positive
# checks and service interruption.
timeoutSeconds: 5
# As per Kubernetes documentation (https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#when-should-you-use-a-startup-probe),
# startup probe should point to the same endpoint as the liveness probe.
#
# Startup probe is only needed when container is taking longer to start than
# `initialDelaySeconds + failureThreshold Γ periodSeconds` of the liveness probe.
startupProbe:
httpGet:
path: /live
port: 8080
failureThreshold: 3
initialDelaySeconds: 10
periodSeconds: 30
successThreshold: 1
timeoutSeconds: 5
Thanks
β Terminus
β Lightship
β Stoppable
β Bret Fisher for his great articles and videos
β Cloud Health
Sponsors
Donate
If you like my job, don't hesitate to contribute to this project! β€οΈ