@meetbit/express-backend

1.0.17 • Public • Published

express-backend

npm version Test Release

This boilerplate will you get started with a production-ready expressjs backend server complete with the following:

  • absolute imports
  • modular file structure
  • live-reloading
  • automated testing
  • custom production-ready error handler
  • custom logger that interfaces with CloudWatch
  • many more

This uses multiple other packages to work. Their documentation will be linked below.

If something does not work, please file an issue.

If you need help or have some suggestions, let us know in Discussions.

Express-Backend is maintained and developed by MeetBit Inc.

Quick Start

npx @meetbit/express-backend my-server

Make sure you have Node 16.14.0 and Yarn 1.22.x. This will create a new project called my-server, install required packages and run your server for the first time.

Screen Shot 2022-03-06 at 11 35 19

This runs the server in PORT=3001 to avoid clashes with most frontend frameworks like ReactJS. You can start testing your new server by running a GET / request using Insomnia or Postman. Or, you can just type [http://localhost:3001](http://localhost:3001) on your browser.

You can start creating routes by adding modules in ./routes and following how the calendar route is coded.

Creating Your Server

You’ll need to have Node 16.0.0 or later version but we recommend running Node 16.14.0 specifically for your local and production environments. You must also have Yarn 1.2x.x installed. It is a personal preference we have at MeetBit to use Yarn but feel free to switch to npm if you prefer. You can follow this tutorial.

npx

npx @meetbit/express-backend my-server

(npx is a package runner tool that comes with npm 5.2+ and higher, see instructions for older npm versions)

This creates a folder with a structure outlined below and installs required dependencies using Yarn. This also runs your server on PORT=3001 by default to avoid clashes with the default ports of most frontend frameworks (PORT=3000).

Environment Variables

express-backend does not require any environment variables to get started. But the following can be configured for customization and is required for production.

Key Development Value
ENV dev (default) | prod | staging
PORT 3001 (default)
AWS_ACCESS_KEY_ID *generate your own
AWS_SECRET_ACCESS_KEY *generate your own
AWS_REGION *generate your own

(The required AMI Permissions used for logging to CloudWatch be available below.)

File Structure

my-server/
├── bin/
├── controllers/
├── middlewares/
│   ├── middlewareA/
│   │   └── index.js
├── public/
│   └── images/
│       └── logo-32.png
├── routes/
│   ├── calendar/
│   │   ├── calendar.js
│   │   ├── calendar.test.js
│   │   └── index.js
│   └── index.js
├── tests/
│   └── app.test.js
├── utils/
├── ├── logger/
│   │   └── index.js
├── .env
├── .gitignore
├── app.js
├── jsconfig.json
├── LICENSCE
├── package.json
├── README.md
└── yarn.lock

Express-Backend follows a modularized version of the Model-View-Controller (MVC) pattern where instead of separating routers and controllers, we join them in the same module including the route’s tests. To explain each file/folder:

  • /bin → holds www file to run your server.
  • /controllers → holds general controllers that you think might be used by several routes. (might not be very necessary with modules but just in case).
  • /middlewares → holds scustom middlewares here. catchAsync and globalLogger is included.
  • /public → holds static assets like images, etc.
  • /routes → holds your modularized routes.
  • /tests → holds general application tests.
  • /utils → holds general utilities. A custom winston logger is included.

In case you’re wondering, the example route provided is calendar because that’s what we deal with at MeetBit.

Live-Reloading and Running Your Server

yarn dev

Express-Backend has been configured to use nodemon when running your server on during development. Please not that this is NOT hot-reloading. nodemon restarts your server every time it notices changes in your code (except in /tests) so session data, states and other ephemeral data will not be stored. Your frontend application will also be needed to be refreshed separately.

To learn more on how to configure and customize nodemon, read its documentation.

Absolute Imports

// app.js
...
const globalLogger = require('#middlewares/globalLogger')
const indexRouter = require('#routes/index')

Express-Backend has been configured to allow absolute imports without any additional packages or typescript. To use, simple follow the syntax above and append an # before the folder name in root.

Please note that this was only preconfigured to be used on index.js files of modules in the locations outlines below and on any file in root. Nesting is also allowed and works (e.g. #routes/routeA/subRouteB )

  • #root (any file)
  • #controllers
  • #middlewares
  • #public
  • #routes
  • #tests
  • #utils

Making Modifications

The main way we are making this work is with subpath imports in package.json and jsconfig.json for intellisense so you will need to make modifications to both files. At the same time, you will need to make adjustments to jest: {... moduleNameMapper: { ... } } in package.json for tests to work. You can follow the following syntax for both files.

//package.json

...
//subpath imports
"imports": {
  "#root/*": "./*",  // this will allow you to reference all files but you will also have to mention the filename itself. e.g. require(`#root/app.js`)
  "#controllers/*": "./controllers/*/index.js", // this is perfect for modules and there will be no need to mention index.js. e.g. require(`#routes/calendar`)
	...
},

...
//jest moduleNameMapper
"jest": {
    ...
  "moduleNameMapper": {
    "#root/(.*)": "<rootDir>/$1", // <rootDir> references the root directory
    "#controllers/(.*)": "<rootDir>/controllers/$1", // $1 references the wildcard that replaces (.*)
  }
},

...
//jsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "#root/*": ["./*"],
      "#controllers/*": ["./controllers/*"],
    }
  }
}

Why #? This is because subpath imports use the # prefix compared to module-alias’ @ prefix. We opted to use subpath imports for two main reason: (1) less packages; and (2) differentiation between npm packages starting with the @ prefix.

Pre-Built Middleware

Express-Backend has three pre-built middleware for you, catchAsync, globalLogger and newError.

catchAsync

//calendar.js

exports.getCalendar = catchAsync(async (req, res, next) => {
	//do some api calls
	
	//logger
	res.status(200).send('Successfully got calendar')
})

The general usage for catchAsync is outlined above and it is used to remove the need for a try-catch block around an entire controller that makes requests to an API. To use, follow syntax above and wrap your asynchronous controller in catchAsync.

globalLogger

2022-03-05T20:59:07.720Z info: [requestId-xxYY] POST /calendar Request Received
2022-03-05T20:59:07.732Z info: [requestId-xxYY] POST /calendar Responded in 5.506ms

Every request that comes in and when responses are sent out is logged with globalLogger. The response time is also included at the response log. The above shows how both logs will show on your console. More information about how logging can be done is available below.

newError

2022-03-05T20:59:25.843Z error: [requestId-xxYY] POST /calendar 500 Internal Server Error-Something went wrong in creating your calendar.

We implemented an error-handling middleware to process all errors. It builds on top of http-errors and logs errors using our custom logger and comes preconfigured. Just make sure to keep app.use(errorHandler) in app.js at the end of the file after all other middleware and routes. Here’s the error-handler’s standard behavior:

  • In production, the default error status code is 500 unless specified.
  • In production, any http errors with status codes >500 is reverted to 500.
  • In production, only the status code and error message is logged and returned in the response.
  • In development and staging, instead of the error message, a stack trace is logged and returned in the response.

newError uses http-error so you can follow the standard syntax of createError() to throw errors:

if (!user) return next(createError(401, 'Unauthorized. Please login.'))

In addition to the above three middlewares, express-backend and configures the following useful middlewares for you:

  • helmet
  • compression
  • cors
  • express.json
  • express.urlencoded
  • cookieParser
  • responseTime

Custom Winston Logger with CloudWatch

Express-Backend has a pre-built custom logger that logs request information along with your message for easily tracing errors and requests in production. It uses winston and winston-cloudwatch to interface with AWS CloudWatch in production.

Setup

In order to use our custom logger in production, you have to setup your environment variables to connect to CloudWatch. AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_REGION are required. The minimum AMI Permissions required for the logger are:

  • DescribeLogStreams
  • CreateLogGroup
  • CreateLogStream
  • PutLogEvents
  • PutRetentionPolicy

Usage

const logger = require('#utils/logger')

const thisIsAController = (req,res,next) => {
	logger.error(`message`, req) // with request object
}

logger.info(`message outside a controller`) // without request object

Using the logger is quite simple and can be used to completely replace console.log even in development. Whenever you are in a controller or have access to the HTTP request object, you can pass it as the second parameter and the request information will be logged together with your message. This is optional and you can still log messages without it. The logger follows winston’s logging levels (based on npm logging levels) as laid out below:

level priority
error 0
warn 1
info 2
http 3
verbose 4
debug 5
silly 6

Logging Behavior

2022-03-05T21:46:51.731Z info: RUNNING express-backend v1.0.0 on PORT 3001
2022-03-05T21:18:17.376Z info: [requestId-xxYY] POST /calendar - Request Received
2022-03-05T21:18:17.379Z error: [requestId-xxYY] POST /calendar - 500 Internal Server Error-InternalServerError: Something went wrong in creating your calendar.
    at /express-backend/routes/calendar/calendar.js:24:32
    at /express-backend/middlewares/catchAsync/index.js:27:5
		...long stacktrace
2022-03-05T21:18:17.392Z info: [requestId-xxYY] POST /calendar - Responded in 5.415ms

The above shows how logs are viewed from the console in development. As mentioned in newError above, the stack trace is logged when errors occur in development and staging. The first line also illustrates logs that do not come from within a controller.

Screen Shot 2022-03-06 at 05 49 13

The above shows how logs are viewed from CloudWatch for production. Staging are similar except: (1) they log the stack trace when an error occurs; and (2) the Log Group Name is automatically is suffixed with -staging (e.g. express-backend-staging). The logger also handles running your server in clusters and separates each instance on a separate Log Stream.

Request Information

The biggest advantage that our custom logger brings is its ability to log request information along with your logs. This allows you to easily search and filter for certain logs from within CloudWatch (and even your console). By default, the logger includes the following information on every log:

  • log level
  • request ID
  • request method
  • request path

Something that might be unfamiliar for most of you is the requestId inside the brackets. This is not standard but is something we do at MeetBit to trace all logs belonging to a single request and allows us to connect an event from the frontend to a particular request. To use this, simply add x-request-id to your headers.

Testing

yarn test

Express-Backend comes with jest, supertest and jest-runner-groups for automated testing. Running the command above will run all *.test.js files within your project. Refer to jest and supertest, and jest-runner-groups’ documentation for an in-depth explanation of how to setup your tests. A general setup can be seen in tests/app.test.js and route/calendar/calendar.test.js.

Modular Testing

//calendar.test.js

/**
 * Main Test File.
 * 
 * @group unit
 * @group integration
 * @group calendar
 */

 const request = require("supertest");
 const app = require("#root/app");
 
 const { nanoid } = require('nanoid')
 
 describe("Testing calendar functions.", () => {
   test("GET /", (done) => {
     request(app)
       .get("/calendar/123")
       .set('x-request-id', `test-${nanoid(6)}`)
       .expect(200)
       .end((err, res) => {
         if (err) return done(err);
         return done();
       });
   })

	... other tests
})

Similar to how we recommend you store your controllers and routes under the same folder as a module, we also recommend you write your tests in a modular way. You may reserve tests/* for general app-wide tests that check functionality across multiple facets of your app. To do this, you can simply add groups to the test files to which tests the files are a part of. The example above indicates the test will run for unit, integration and calendar tests.

Grouping Tests

yarn test --group=unit --group=sanity

In connection to the previous section, running tests for a particular group is quite easy. You just have to add the --group flag to the test command and indicate which tests you want to run. The example above will run tests that are a part of unit and/or sanity. You may also opt to just run tests on a single group.

Contributing

We are open for contributions! We’re not yet sure how to go about this so just feel free to create a pull request.

Sponsors

Support Express-Backend by contributing financially as a sponsor. Sponsor a monthly amout here and we'll add you in this README.

Resources

This file references documentation from the different dependencies that this project uses. Here are links to the most important ones you will need as you develop your server.

License

Express-Backend is open source software by MeetBit Inclicensed as MIT .

Readme

Keywords

none

Package Sidebar

Install

npm i @meetbit/express-backend

Weekly Downloads

0

Version

1.0.17

License

MIT

Unpacked Size

36.9 kB

Total Files

30

Last publish

Collaborators

  • lvillacin