Robust Serverless API Boilerplate with ES6, Folder Structure, Testing (Mocha + Chai), and ESLint
Basically before I start any serious project I like to have a few things setup:
- ES6/ES7 Webpack and Babel (From Starter)
- A good offline dev workflow (From Starter)
- ESLint
- Folder structure
- Testing
I hope this isn't too obscure of a topic, but I'm not actually going to cover how to build a CRUD API, this is just an example starting boilerplate before you begin coding your project.
I'm assuming you've at least tried using the serverless framework before. If you are new to serverless, I have an article that breaks it down from the beginning here:
Serverless Back-End for React - Your Introduction to Serverless Architecture
#The Starter
Lets start with the great Serverless Node.js Starter (github) from the awesome Serverless Stack project. If you're not familiar with that project be sure to check it out.
The starter basically includes all this stuff (From the the starter description page):
- Use ES7 syntax in your handler functions
- Package your functions using Webpack
- Run API Gateway locally
- Use
serverless offline start
- Use
- Support for unit tests
- Run
npm test
to run your tests
- Run
- Sourcemaps for proper error messages
- Error message show the correct line numbers
- Works in production with CloudWatch
- Automatic support for multiple handler files
- No need to add a new entry to your
webpack.config.js
- No need to add a new entry to your
- Add environment variables for your stages
If you dont use this starter, you have to add a lot of this stuff one by one by including and configuring the right packages and plugins. So this gives us a great place to start.
#Create a New Project
First make sure you have the serverless
module installed globally
yarn global add serverless
# or
npm install serverless -g
And then run this command to create a new serverless project using the starter:
$ serverless install --url https://github.com/AnomalyInnovations/serverless-nodejs-starter --name my-project
It gives us a serverless.yml
file that looks like this:
service: my-project
plugins:
- serverless-webpack
- serverless-offline
custom:
webpack:
webpackConfig: ./webpack.config.js
includeModules: true
provider:
name: aws
runtime: nodejs8.10
stage: dev
region: us-east-1
functions:
hello:
handler: handler.hello
events:
- http:
path: hello
method: get
And a handler.js file that looks like this:
export const hello = async (event, context, callback) => {
const response = {
statusCode: 200,
body: JSON.stringify({
message: `Go Serverless v1.0! ${(await message({ time: 1, copy: 'Your function executed successfully!'}))}`,
}),
};
callback(null, response);
};
const message = ({ time, ...rest }) => new Promise((resolve, reject) =>
setTimeout(() => {
resolve(`${rest.copy} (with a delay)`);
}, time * 1000)
);
It also gives us a test folder with an example test.
// tests/handler.test.js
import * as handler from '../handler';
test('hello', async () => {
const event = 'event';
const context = 'context';
const callback = (error, response) => {
expect(response.statusCode).toEqual(200);
expect(typeof response.body).toBe("string");
};
await handler.hello(event, context, callback);
});
We can run this command to start the offline server for a good development workflow:
```bash
serverless offline start
This will start an offline server that you can use to make API requests and test all the endpoints.
I also like to add a script in the package.json
file to make this easy to launch:
"scripts": {
"start": "serverless offline start",
"lint": "node_modules/.bin/eslint .",
"test": "NODE_ENV=test node_modules/.bin/mocha --recursive --require babel-core/register"
},
#Add ESLint
I always work with a linter these days, there's no better way to keep clean code and enforce best practices with a language like javascript where its so easy to write messy and ugly code.
Lets add eslint
and some plugins:
yarn add --dev eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-mocha eslint-plugin-promise
And then make a new .eslintrc.json
file in the root of our project.
touch .eslintrc.json
And add this to the new file:
{
"extends": ["airbnb/base", "plugin:promise/recommended"],
"plugins": ["promise"],
"rules": {}
}
Add a .eslintignore
file:.
touch .eslintignore
And ignore the webpack config:
# .eslintignore
webpack.config.js
And then add a lint
script to our package.json
:
"scripts": {
"start": "serverless offline start",
"lint": "node_modules/.bin/eslint .",
},
Then you can run the linter with this command:
yarn lint
# or
npm run lint
#API Folder Structure
When I'm building a serverless api I like to give my functions, paths, and folder a api-like structure. So I'll create some directories to organize my handlers. This may be a bit overkill for small projects, but I dont think there are any downsides of having this kind of extra organization right off the bat.
mkdir -p handlers/api/v1/todos
I think you can think of these handlers as controllers, and so if you're building CRUD endpoints for a model, you can add a different file for each of the 5 main API actions:
touch handlers/api/v1/todos/index.js
touch handlers/api/v1/todos/show.js
touch handlers/api/v1/todos/create.js
touch handlers/api/v1/todos/update.js
touch handlers/api/v1/todos/delete.js
Then the functions part of your serverless.yml
file would look something like this:
functions:
api/v1/todos/index:
handler: handlers/api/v1/todos/index.default
events:
- http:
path: api/v1/todos
method: get
api/v1/todos/show:
handler: handlers/api/v1/todos/show.default
events:
- http:
path: api/v1/todos/{id}
method: get
api/v1/todos/create:
handler: handlers/api/v1/todos/create.default
events:
- http:
path: api/v1/todos
method: post
api/v1/todos/update:
handler: handlers/api/v1/todos/update.default
events:
- http:
path: api/v1/todos/{id}
method: put
api/v1/todos/delete:
handler: handlers/api/v1/todos/delete.default
events:
- http:
path: api/v1/todos/{id}
method: delete
As you can see this is a pretty typical REST setup.
We can now remove our original handler.js file since we dont need it anymore:
rm handler.js
We wont be using this today, but lets also create a models
folder where I can put our models:
mkdir models
touch models/todo.js
Then the individual handlers will include the models to handle the crud operations.
Now I'm going to add a basic handler for todos/index.js
so we have something to test:
# handlers/api/v1/index.js
export default (event, context, callback) => {
const response = {
statusCode: 200,
body: JSON.stringify({
message: 'Hello from todos/index',
}),
};
callback(null, response);
};
And if you are following along, you'll have to add an export to all the handler files in order to run the offline server.
#Setup Testing
By default the starter comes with jest
but I prefer mocha
so lets swap jest
for mocha
and add a bit of chai
for the assertions.
mv tests test
rm test/handler.test.js
yarn remove --dev jest
yarn add --dev mocha chai
touch test/test_helper.js
Now lets add some test files for our endpoints:
mkdir -p test/api/v1/todos/
touch test/api/v1/todos/index.test.js
touch test/api/v1/todos/show.test.js
touch test/api/v1/todos/create.test.js
touch test/api/v1/todos/update.test.js
touch test/api/v1/todos/delete.test.js
And we'll only add 1 test for now for our todos/index
handler:
import { expect } from 'chai';
import todosIndex from '../../../../handlers/api/v1/todos';
describe('Fetching list of todos', () => {
it('returns a valid response', (done) => {
const event = 'event';
const context = 'context';
const callback = (error, response) => {
expect(response.statusCode).to.equal(200);
expect(typeof response.body).to.equal('string');
expect(response.body).to.contain('Hello from todos/index');
done();
};
todosIndex(event, context, callback);
});
});
Then we can run it by first adding this script to our package.json
file:
"scripts": {
"start": "serverless offline start",
"lint": "node_modules/.bin/eslint .",
"test": "NODE_ENV=test node_modules/.bin/mocha --recursive --require babel-core/register"
},
We have to add the --recursive
tag so it will find the tests in our subfolders, and the --require babel-core/register
tag to make ES7 work with our tests.
Then we can run our tests with the command:
yarn test
# or
npm test
Now we also need a different .eslintrc.json
file for our testing so if we put a new one in our test tile the tests will play by different eslint rules.
touch test/.eslintrc.json
With these contents so it will ignore mocha keywords:
{
"extends": ["airbnb/base", "plugin:promise/recommended"],
"plugins": ["promise", "mocha"],
"env": {
"mocha": true
},
"rules": {}
}
#Wrap it up
Now we can start our offline server:
serverless offline start
We can test our code:
yarn test
# or
npm test
...and we can lint our code:
yarn lint
# or
npm run lint
And we have some good folder structure to start building our API.
Thanks for reading, I hope this can be of help to someone.
As always if you notice anything I did that could be improved, please reach out. I'm always looking to improve.