Microservice template
- Pre-reqs
- Development NPM commands
- Libraries and Framworks
- Project Structure
- Serializing Jest Snapshots
- Mocking with Jest
- Dependencies
- Licence
- Nodejs 16.x and later (but this should work with older versions as well)
- MongoDB 4.x
- VSCode or Webstorm
Command | Description |
---|---|
npm run dev |
Development mode |
npm run lint |
Checks linting and formatting issues in ./src |
npm run pretty |
Fixes formatting of any ts files inside ./src |
npm run test |
Runs all unit and integrations tests eg: ./src/api/**/**.test.ts |
Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
Almost all NodeJs developers know how to use Express, it is perfect for API microservice because of the small footprint with lots of third-party
support. Express is also the lowest common denominator for all tools when it comes to NodeJs. If you talk about Ruby, they have Rails,
Python has Django and PHP has Laravel, and NodeJs is synonymous with Express. One downside for Express over Koa is that it doesn't handle
async errors, all other frameworks based on Express have this kind of flaw as well.
It is already fixed in the next version but adding a quick catchasync
function can easily fix this issue without the need of try/catches
// 👎 too many redundant try/catch
export const getData = async function (req: Request,res: Response) {
const { id } = req.body;
try{
const user = await User.findOne({ _id:id });
...
..
.
}catch(e){
throw new GhErrorOther('Invalid ID');
}
res.jsonp(user);
};
// 👍
export const getData = catchAsync(async function (req: Request,res: Response) {
const { id } = req.body;
const user = await User.findOne({ _id:id });
// automatically sends error 500
ghAssert(user,GhErrorOther,'Invalid ID');
// we are sure that user is found by the time the code executes after ghAssert
res.jsonp(user);
});
// 👎 not bad but we can make it better
export const getData = async function (req: Request,res: Response) {
const { id } = req.body;
const user = await User.findOne({ _id:id });
if(user){
return res.status(422).send({message:'User is not found'})
}else{
res.jsonp(user);
}
};
// 👍 lesser scope and if/else statements
export const getData = catchAsync(async function (req: Request,res: Response) {
const { id } = req.body;
const user = await User.findOne({ _id:id });
// automatically sends error 422
ghAssert(user,GhValidationError,'User not found');
// we are sure that user is found by the time the code executes after ghAssert
res.jsonp(user);
});
Joi is a powerful schema description language and data validator for JavaScript. For validation, Joi can easily integrate with ExpressJs as a middleware.
// module-name.routes.ts
// validator function automatically returns 422 for invalid values
moduleRouter.post('/', validator(myValidator), functionName);
// module-name.validator.ts
export const myValidator = {
schema: Joi.object({
name: Joi.string().required(),
}),
};
Jest is a delightful JavaScript Testing Framework with a focus on simplicity. Compared to other testing frameworks like Mocha, Jest has more features and doesn't need other libraries like code coverage and mocking.
Snapshot Testing - Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly. Although primarily used in React, snapshot testing is a very good tool for APIs. Having a snapshot for API responses will make maintenance easier.
With snapshots, new changes can be monitored and updated if needed. Adding new field will immediately fail test integration.
● Pokemon Tests › CRUD routes › CREATE - Should respond with status code 200
expect(received).toMatchSnapshot()
Snapshot name: `Pokemon Tests CRUD routes CREATE - Should respond with status code 200 1`
- Snapshot - 1
+ Received + 0
@@ -2,9 +2,8 @@
"__v": 0,
"_id": "000000000000000000000000",
"createdAt": "1984-01-24T16:00:00.000Z",
"deleted": false,
"name": "Pikachu",
- "type": "Electric",
"updatedAt": "1984-01-24T16:00:00.000Z",
"user": "000000000000000000000000",
}
33 | .set('Authorization', `Bearer ${token}`);
34 | expect(response.status).toEqual(200);
> 35 | expect(response.body).toMatchSnapshot();
| ^
36 | createdId = response.body._id;
37 | });
38 |
beforeAll(async () => {
await mongo.dropAllCollections();
// create user
const fakeUser = await createFakeUser(false);
token = fakeUser.token;
});
afterAll(async (done) => {
await mongo.dropAllCollections();
await mongo.close();
done();
});
describe('Pokemon Tests', () => {
describe('CRUD routes', () => {
test('CREATE - Should respond with status code 200', async () => {
const response = await request(app)
.post('/')
.send(json)
//
.set('Authorization', `Bearer ${token}`);
expect(response.status).toEqual(200);
expect(response.body).toMatchSnapshot();
createdId = response.body._id;
});
});
PASS src/api/phone/phone.test.ts
Pokemon Tests
CRUD routes
✓ CREATE - Should respond with status code 200 (50 ms)
✓ CREATE WITHOUT TOKEN - Should respond with status code 401 (20 ms)
✓ READ ONE - Should respond with status code 200 (26 ms)
✓ READ ONE WITHOUT TOKEN - Should respond with status code 401 (8 ms)
✓ PAGINATION - Should respond with status code 401 (21 ms)
✓ PAGINATION WITHOUT TOKEN - Should respond with status code 401 (7 ms)
✓ UPDATE ONE - Should respond with status code 200 (16 ms)
✓ UPDATE ONE WITHOUT TOKEN - Should respond with status code 401 (8 ms)
✓ DELETE ONE - Should respond with status code 200 (13 ms)
✓ DELETE ONE WITHOUT TOKEN - Should respond with status code 401 (6 ms)
------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------------|---------|----------|---------|---------|-------------------
All files | 91.11 | 0 | 55.55 | 91.11 |
api/pokemons | 100 | 100 | 100 | 100 |
pokemon.controller.ts | 100 | 100 | 100 | 100 |
pokemon.model.ts | 100 | 100 | 100 | 100 |
pokemon.routes.ts | 100 | 100 | 100 | 100 |
pokemon.validator.ts | 100 | 100 | 100 | 100 |
server | 86.88 | 0 | 50 | 86.88 |
app.ts | 91.66 | 0 | 33.33 | 91.66 | 36,48,53
mongo.ts | 75 | 100 | 75 | 75 | 9-12
routes.ts | 88.88 | 100 | 0 | 88.88 | 13
------------------------|---------|----------|---------|---------|-------------------
Test Suites: 2 passed, 2 total
Tests: 20 passed, 20 total
Snapshots: 8 passed, 8 total
Time: 4.088 s
ESLint is a code linter which mainly helps catch quickly minor code quality and style issues.
Like most linters, ESLint has a wide set of configurable rules as well as support for custom rule sets.
All rules are configured through .eslintrc
configuration file.
Like the rest of our build steps, we use npm scripts to invoke ESLint. To run ESLint you can call the main build script or just the ESLint task.
npm run build // runs full build including ESLint
npm run lint // runs only ESLint
The most obvious difference in a TypeScript + Node project is the folder structure.
In a TypeScript project, it's best to have separate source and distributable files.
TypeScript (.ts
) files live in your src
folder and after compilation are output as JavaScript (.js
) in the dist
folder.
The full folder structure of this app is explained below:
Name | Description |
---|---|
.vscode |
Contains VS Code specific settings |
.github |
Contains GitHub settings and configurations, including the GitHub Actions workflows |
.dockerignore |
Ignore list for Docker during build process |
.eslintignore |
Ignore list for eslint during linting process |
.eslintrc |
Config settings for ESLint code style checking |
.prettierrc |
Prettier config |
dist |
Contains the distributable (or output) from your TypeScript build. This is the code you ship |
Dockerfile |
Docker file |
jest.config |
Jest config |
LICENSE |
License file |
nodemon.json |
Nodemon config |
node_modules** |
Contains all your npm dependencies |
src** |
Contains your source code that will be compiled to the dist dir |
src/libraries** |
Common utilities and helpers |
src/jest/** |
Jest configuration directory |
src/jest/serializer.ts |
Jest snapshot serializer |
src/jest/setup.ts |
Jest setups configs and mocks |
src/api/**/** |
Modules Directory |
src/api/<module name>/**.controller.ts |
Controllers define functions that respond to various http requests |
src/api/<module name>/**.model.ts |
Models define Mongoose schemas that will be used in storing and retrieving data from MongoDB |
src/api/<module name>/**.routes.ts |
Module routes |
src/api/<module name>/**.types.ts |
Holds .d.ts files not found on DefinitelyTyped. Covered more in this section |
src/api/<module name>/**.test.ts |
Contains your tests |
src/api/<module name>/**.validator.ts |
Joi schema validators |
src/public** |
Static assets that will be used client side |
src/server/** |
All server related code |
src/server/app.ts |
Express app initialization |
src/server/config.ts |
Express app configs and merging of environment variables from **.env |
src/server/mongo.ts |
Mongoose setup and config, includes dropping of collections for testing |
src/server/routes.ts |
Express routes main entry |
src/index.ts |
Express main entry point |
**.env |
Env variables |
jest.config.js |
Used to configure Jest running tests written in TypeScript |
package.json |
File that contains npm dependencies as well as build scripts |
tsconfig.json |
Config settings for compiling server code written in TypeScript |
Since snapshot is a simple file diff, there maybe times wherein the response object will change every build.
Let's say _id will always be dynamic and cannot be fixed in each test.
The file jest.serializer.ts
will replace all dynamic objects values ONLY in the snapshot without mutating the actual response.
Example below will fail because createdAt and updatedAt with different values in every test.
- Snapshot - 2
+ Received + 2
Object {
"_id": "000000000000000000000000", // serialized
- "createdAt": "1984-01-24T16:00:00.000Z", // not serialized
+ "createdAt": "2022-06-10T08:11:46.117Z",
"deleted": false,
"name": "mimikyu",
- "updatedAt": "1984-01-24T16:00:00.000Z",
+ "updatedAt": "2022-06-10T08:11:46.117Z",
"user": "000000000000000000000000",
}
Default object properties are serialized even in a nested object.
- _id
- token
- createdAt
- updatedAt
- password
- user
Mock functions allow you to test the links between code by erasing the actual implementation of a function, capturing calls to the function (and the parameters passed in those calls), capturing instances of constructor functions when instantiated with new, and allowing test-time configuration of return values
Manual mocks are used to stub out functionality with mock data. For example, instead of accessing a remote resource like a website or a database, you might want to create a manual mock that allows you to use fake data. This ensures your tests will be fast and not flaky.
If the module you are mocking make sure to add it in ./src/jest/jest.setup.ts
, read more about mocking here.
.
├── config
├── __mocks__
│ └── fs.js
├── models
│ ├── __mocks__
│ │ └── user.js
│ └── user.js
├── node_modules
└── views
Dependencies are managed through package.json
.
In that file you'll find two sections:
Package | Description |
---|---|
@keithics/auth | Utility auth library for Keithics |
@keithics/code | Core library for Keithics, includes CRUD class |
@keithics/errors | Error library for Keithics eg: assert |
@keithics/joi | Common joi validation schemas for Keithics |
cors | Express 4 cors middleware. |
date-fns | Lightweight JS date parsing utilities |
dotenv | Loads environment variables from .env file. |
express | Node.js web framework. |
helmet | Expresss middleware for http security |
joi | Validation utility library. |
mongoose | MongoDB ODM. |
mongoose-delete | MongoDB middleware for soft deletes |
mongoose-paginate-v2 | MongoDB middleware for cursor pagination |
morgan | Logging library |
Package | Description |
---|---|
@types/* | Dependencies in this folder are .d.ts files used to provide types |
@typescript-eslint/* | Eslinst TS plugins |
babel-eslint | Babel TS plugins |
chai | Testing utility library that makes it easier to write tests |
chalk | Utility that styles Terminal output |
codelyzer | Eslinting utility |
eslint | Linter for JavaScript and TypeScript files |
eslint-config/* | Eslint config |
eslint-plugin* | Eslint plugins |
nodemon | Utility that automatically restarts node process when it crashes |
prettier | An opinionated code formatter |
pretty-format | Stringify any JavaScript value for prettier |
supertest | HTTP assertion library. |
ts-jest | A preprocessor with sourcemap support to help use TypeScript with Jest. |
ts-node | Enables directly running TS files. |
ts-node-dev | Enables directly running TS files during development |
tslint | Linter for TypeScript files |
typescript | JavaScript compiler/type checker that boosts JavaScript productivity |
- Jest RANDOMBYTESREQUEST error in local machine please see here
Copyright (c) Keithics. All rights reserved.