npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

effectsloop-server-utils

v1.8.0

Published

Utilities useful for spinning up servers

Readme

Who Is This For?

I made this for myself as I spin up new servers for solo projects. So it's a bit opinionated.

It's served me well so I thought I'd make it available to anyone who wants it. Also, I wanted access to it as an NPM package, and splurging to make this lib private felt unnecessary.

It uses Koa under the hood because I like how lightweight it is, so this is also for people who are not wed to Express.

What Is Its Purpose?

  • Building an extendable server with some default middleware, and an optional logging middleware (buildServer).
  • Building a logger than can be used throughout your app and consumed by buildServer (buildLogger).
  • Building routes that you pass to the server builder (buildRouter).
  • Handling errors with a syntax that is self-documenting (handleError).

Core Concepts

  • Information is persisted across a request's lifespan via the request's ctx.state object.
    • When the core buildServer function's default logging middleware is left enabled and the optional handleError fn is used, you will find that your response status is reflected in your logging level (info, warn, error), as well as some other niceties. This is thanks to information that is persisted to ctx.state.warning and ctx.state.err.
  • Every request has a unique UUID assigned to it, stored in ctx.state.id.
  • Every request's start time is stored in ctx.state.start as a Date.now() value.
  • It should be simple to tell what types of responses you'll get from a route, so the handleError is intended to receive arguments in a way that is relatively self-documenting. Unless you specifically specify otherwise, the error message from a thrown error will be returned in the server response body.
  • I assume that every request body is either empty or an object.
  • I assume that every response body is either empty or an object.

API

buildServer

Used to build the base Koa server and Bunyan logger, with optional middleware.

Ex:

const { app, log } = buildServer({ name: 'my-server', port: 8080, routes: routesArray })

Arguments

| Name | Type | Default | Description | | ---- | ---- | ------- | ----------- | | port * | Number | Required | The port for the server to listen on. | | name * | String | | Server name for use in logging. Is REQUIRED if you do not provide a log argument, as buildServer expects one of the two. | | log | Bunyan Logger | | The Bunyan logger you would like to use to log requests. Recommended to be generated by buildLogger. | | logPath | String | | The folder path where you want to store your log files. | | | | | When not passed, output is sent to the console. | | | | | When passed, output is sent to a file in the specified directory with the syntax [logPath].log. The log file is rotated out once per day, and 3 days of logs plus the current day are held as per the Bunyan spec. | | routes | Array[buildRouter()] | | Array of routes to use built with the buildRouter API fn. | | beforeStartup | Function(log) => Promise | | Anything you want to happen before we start listening on a port, such as connecting to a DB. It must be a function that returns a Promise. The function is passed the log for use before the server starts listening. | | noLogging | Boolean | false | Whether you want each request to be logged via the logging middleware. | | allowCors | Boolean | false | Whether you want to allow CORS for the server. | | middleware | Array[fn()] | | Array of middleware you would like to use. They are applied right before your routes. | | requestBodyMaxLoggingLen | Number | 500 | The max length of the request body that you want to log - anything afterwards is truncated. Only matters if noLogging is not set. | | responseBodyMaxLoggingLen | Number | 500 | The max length of the response body that you want to log - anything afterwards is truncated. Only matters if noLogging is not set. |

Returns

| Name | Type | Description | | ---- | ---- | ----------- | | app | Koa server | This way you can do anything else you want to the server. | | log | Bunyan logger | This is used by the request/response logging middleware, and is also provided here for you to call as you want. |

Logging In Depth

When the logging middleware is enabled, each request gains several features:

  • Access to a logger which automatically includes the id of the request at ctx.state.log. It enables ctx.log.info, ctx.log.warn, and ctx.log.error calls from within routes.
  • Logging an info output on successful requests, a warn on requests with ctx.state.warning, and an error on requests with ctx.state.err. Because of this, I recommend using this middleware in conjunction with the handleError function that is also exposed by this library, as it leverages the paradigm.
  • Logging the following information upon completion of every request:

| Name | Type | Description | | ---- | ---- | ----------- | | responseStatus | Number | The response status. | | method | String | The request method. | | url | String | The request url. | | body | String or Object | The request body. | | | | Top-level token and password keys are logged as *******. | | | | Because request bodies can get long, logs of them are truncated to 500 characters by default. You can override this on a per-request basis with ctx.state.requestBodyMaxLoggingLen, or for all requests with the requestBodyMaxLoggingLen argument as described above. | | ip | String | The IP of the request. | | responseTime | Number | The response time in ms. | | responseBody | Object or Undefined | The response body. | | | | Top-level token and password keys are logged as *******. | | | | Because response bodies can get long, logs of them are truncated to 500 characters by default. You can override this on a per-request basis with ctx.state.responseBodyMaxLoggingLen, or for all requests with the responseBodyMaxLoggingLen argument as described above. | | id | String | The UUID of the request. |

buildLogger

Used to build a Bunyan logger.

Ex:

const log = buildLogger({ name: 'my-server', logPath: './optionalLogFolderPath' })
const { app } = buildServer({ log, port: 3000 })

log.info('Something to show')

Arguments

| Name | Type | Default | Description | | ---- | ---- | ------- | ----------- | | name * | String | Required | The name of the logger that will be generated (this shows up in logs, and would typically be your server name). | | logPath | String | | The folder path where you want to store your log files. | | | | | When not passed, output is sent to the console. | | | | | When passed, output is sent to a file in the specified directory with the syntax [logPath].log. The log file is rotated out once per day, and 3 days of logs plus the current day are held as per the Bunyan spec. |

Returns

| Name | Type | Description | | ---- | ---- | ----------- | | n/a | Bunyan logger instance. | An instance of a Bunyan logger. |

buildRouter

Used to build a Koa router.

Ex:

const router = buildRouter('/users')

router.post('/sign-up', async ({ request, response, state }) => { ... })

Arguments

| Name | Type | Default | Description | | ---- | ---- | ------- | ----------- | | n/a | String or Object (koa router options) | | When a String, is the route prefix to use. When an Object, is assumed to be koaRouterOptions. |

Returns

| Name | Type | Description | | ---- | ---- | ----------- | | n/a | Router instance | An instance of a Koa Router for you to apply router.post, router.get, etc routes to. |

handleError

Used to handle errors in your routes with a self-documenting API.

Ex:

router.post('/sign-up', async ({ request, response, state }) => {
  try {
    // route logic here
  } catch (err) {
    const options = {
      emailTaken: 409,
      usernameTaken: {
        status: 409,
        message: 'That username is already taken',
        code: 'userTaken',
      },
      defaultMessage: 'Something broke while attempting to sign up',
    }
    handleError({ response, state, err, options })
  }
})

Arguments

| Name | Type | Default | Description | | ---- | ---- | ------- | ----------- | | response * | Object | Required | Koa response. | | state * | Object | Required | Koa state. | | err * | Error | Required | The thrown error. | | msg | Object or String | | The error response to use for all error cases. See below. | | options | Object | | The different possible error responses to use. See below. | | StructError | StructError | | The error class to use for data structure validation checks - an err instanceof StructError check will be used on it, and if there is a match, the passed err shape is expected to match the shape of superstruct errors. |

  • msg:
{
  message: String (non-empty),
  status: Number (optional, 500 default),
  code: String (optional)
}

OR

String (returns 500)
  • options:
{
  [optionCode]:
    {
      status: Number,
      isError: Bool (optional, if true state.err = err, else state.warning = (message || err.message)),
      message: String (optional, overrides err.message as returned error.message),
      code: String (optional, overrides optionCode as returned error.code),
      noCode: Bool (optional, when true no error.code is returned)
    },

    OR

    Number (status to return, which means the thrown err.message and err.code are sent; if >= 500, state.err = err)
  ...
  defaultMessage: same as msg
}

Pass either but not both msg and options.

  • If neither is provided then a 500 is sent automatically with a generic error.
  • If only options are passed, and the thrown error.code key does not match any options (options[error.code]), and no options.defaultMessage is passed, then a 500 is sent automatically with a generic error.
  • If both are provided then a warning is logged and the msg is used.

Responses

Standard errors have the format:

{
  error: {
    message: String (msg.message || msg || options[err.code].message || options[err.code])
    code: String (options[err.code].code || err.code) // Not sent for generic errors, or when options[err.code].noCode is passed
  }
}

Validation (struct) errors have the format:

{
  error: {
    message: String
    fields: {
      [fieldName]: validation error message,
      ...
    }
  }
}

Everything In Action

Define Routes

const { buildRouter } = require('effectsloop-server-utils')

const users = require('./someUserModule')
const handleError = require('./handleError')

const router = buildRouter('/users')

router.post('/sign-up', async ({ request, response, state }) => {
  try {
    // route logic here
  } catch (err) {
    const { validationErrors } = users.signUp

    // The codes for errors that could be thrown, and the info to send back for those errors
    // 4xx errors will log a warning, and the message from the error will be sent in the
    // the response body as { error: { message: error message } }
    // The defaultMessage logs an error, and the response body is { error: { message: defaultMessage } }
    const options = {
      [validationErrors.EMAIL_TAKEN]: 409,
      [validationErrors.USERNAME_TAKEN]: {
        status: 409,
        message: 'That username is already taken',
        code: 'userTaken',
      },
      defaultMessage: 'Something broke while attempting to sign up',
    }

    handleError({ response, state, err, options })
  }
})

router.post('/delete-account', async ({ request, response, state }) => {
  try {
    // route logic here
  } catch (err) {
    // No route-specific errors are anticipated here, so any error results in a 500
    // with { error: { message: msg } }
    handleError({ response, state, err, msg: 'Something broke while attempting to delete account' })
  }
})

module.exports = router

Extend handleError If Needed

const { handleError } = require('effectsloop-server-utils')
const { StructError } = require('superstruct')

const { DBError, ValidationError } = require('../lib/Errors')
const { users } = require('../repo')

module.exports = ({ response, state, err, msg, options }) => {
  // Handle route-agnostic DBErrors here so that you can avoid defining this handling in multiple routes
  if (err instanceof DBError) {
    response.status = 500
    response.body = { error: { message: 'An error occured in the DB', code: 'dbError' } }
    state.err = err // eslint-disable-line no-param-reassign
    return
  }

  // Handle route-agnostic ValidationErrors here so that you can avoid defining this handling in multiple routes
  const { INVALID_EMAIL, DEACTIVATED_ACCOUNT, UNVERIFIED_ACCOUNT } = users.genericValidationErrors
  if (
    err instanceof ValidationError &&
    [INVALID_EMAIL, DEACTIVATED_ACCOUNT, UNVERIFIED_ACCOUNT].includes(err.code)
  ) {
    const { code, message } = err
    response.status = code === INVALID_EMAIL ? 401 : 409
    response.body = { error: { message, code } }
    state.warning = 'Generic user validation failed' // eslint-disable-line no-param-reassign
    return
  }

  // Handle route-specific errors as you've defined in each route
  handleError({ response, state, err, msg, options, StructError })
}

Create The Logger and Server

const { buildLogger, buildServer } = require('effectsloop-server-utils')

const connectToDB = require('./connectToDB')
const userRoutes = require('../userRoutes')

const { SERVER_PORT } = process.env

const connectToDB = log = new Promise((resolve, reject) => {
  // DB connection logic here
  db.on('error', err => {
    log.error({ err }, 'Connection error')
    reject()
  })

  db.on('open', () => {
    log.info('Connected to the DB!')
    resolve()
  })
})

// Instead of building here, could pass `name` to buildServer and get `{ app, log }` from it
const log = buildLogger({ name: 'user-service' })

// The only unused option here is noLogging
const { app } = buildServer({
  log,
  routes: [userRoutes],
  port: SERVER_PORT,
  allowCors: true,
  requestBodyMaxLoggingLen: 300,
  responseBodyMaxLoggingLen: 400,
  beforeStartup: connectToDB,
})

module.exports = { app, log }