• Stars
    star
    271
  • Rank 151,717 (Top 3 %)
  • Language
    TypeScript
  • License
    Other
  • Created almost 5 years ago
  • Updated 5 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Tiny (~5k), KISS, dependency-free Node.JS library to make your API more graceful

πŸš€ Graceful Server 🐒

GitHub package.json version JavaScript Style Guide npm type definitions License npmjs download Donate with PayPal

Tiny (~5k), KISS, dependency-free Node.JS library to make your Rest API graceful.


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

β˜… IBM documentation

β˜… Node HTTP documentation

β˜… Cloud Health

β˜… Cloud Health Connect

Sponsors

JetBrains Logo

Donate

Donate

If you like my job, don't hesitate to contribute to this project! ❀️