Node.js API Server template based on TypeScript.
- Language: TypeScript
- Node: Node.js (v.8: LTS)
- Routing: Express
- Auth: JWT Authentication
- ORM: sequelize
- Testing: Mocha + power-assert
- Node.js (Version 8 LTS)
- Yarn
- direnv
- MySQL
- 100% TypeScript.
- Auth with JWT.
- Written with functions. Class are rarely used in the app.
- Spec files are located within the directory in which the test target file is.
- Written with Promise and Async Await.
- config : stores application settings, such as JWT token algorythm and DB settings.
- controllers : stores request handler functions.
- errors : stores errors.
- helpers : stores helper functions used everywhere in the app.
- middlewares : stores Express middlewares.
- models : stores sequelize Model such as, User, Book.
- scripts : stores scripts called from yarn command.
- spec : stores Factory Function used in spec files.
git clone [email protected]:AtaruOhto/node-api-server-starter.git
cd node-api-server-starter
yarn
cp .envrc.sample .envrc
Please generate a secret string and copy it to .envrc file. Also you can set this as environment variable in OS if you like.
yarn run secret
# copy it to .envrc file
# export SECRET_KEY_BASE=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Default database settings is as follows:
- User: root
- Password: pass
- host: localhost
- port: 3306
You can override these settings by the editing .envrc file.
export DB_USER='root'
export DB_PASS='pass'
export DB_HOST='127.0.0.1'
export DB_PORT=3306
If want to use another database, please edit dialect in src/config/dsatabase.ts.
For more details, sequelize manual dialects.
export const DB_CONFIG = {
...
dialect: 'mysql',
...
};
After editing .envrc as database can be connected, type the following command and load environment variables defined in .envrc .
direnv allow
Then, create database and migration and put seed data into tables.
yarn run db:create
yarn run db:migrate
yarn run db:seed
Start Node.js server. The server runs at port 3000 as default.
yarn start
Let's test. Request the server to issue the JWT token. The returned value is the token which will be used with authentication.
curl -X POST http://localhost:3000/sessions --data 'name=Erich&password=password'
The following is an example of returned value. data will be different in each time.
{
"data":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJoYXNoIjp7Im5hbWUiOiJFcmljaCJ9LCJpYXQiOjE1MzUyMDUzMDIsImV4cCI6MTUzNTI5MTcwMn0.DRCHA1qRwrmpBscw_ZFAde6tBPJEb7IiCso9-mXG2Gk",
"status":200
}
Let's consume API which requires authentication and get all users. Request the server with the string composed by Bearer and blank space and returned value as following:
curl -X GET http://localhost:3000/users -H "X-Auth-Token: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJoYXNoIjp7Im5hbWUiOiJFcmljaCJ9LCJpYXQiOjE1MzUyMDUzMDIsImV4cCI6MTUzNTI5MTcwMn0.DRCHA1qRwrmpBscw_ZFAde6tBPJEb7IiCso9-mXG2Gk"
All user put by seed can be fetched.
{
"data":
[
{"id":9,"name":"Erich","password":"$2b$10$5oo2y/pqQ.NTcaQgL4DF3ODlM3DKDsyiQZgnu5seQS/vUN1lkI8ua"},
{"id":10,"name":"Richard","password":"$2b$10$5oo2y/pqQ.NTcaQgL4DF3ODlM3DKDsyiQZgnu5seQS/vUN1lkI8ua"},
{"id":11,"name":"Ralph","password":"$2b$10$5oo2y/pqQ.NTcaQgL4DF3ODlM3DKDsyiQZgnu5seQS/vUN1lkI8ua"},
{"id":12,"name":"John","password":"$2b$10$5oo2y/pqQ.NTcaQgL4DF3ODlM3DKDsyiQZgnu5seQS/vUN1lkI8ua"}
]
,"status":200
}
If you send a request with wrong token or any invalid request, following response will be returned.
{
"data":{},
"status":400
}
Testing runs with the command. All spec file is located in the directory in which the test target file is stored.
# Create database and do migration, before run testing.
yarn run db:create:test
yarn run db:migrate:test
yarn run db:seed:test
yarn run test
Let's create a new Model and write a controller to handle user request and learn how to develop.
Let's run these commands in another Terminal tab.
# Install npm modules
yarn
# TypeScript Watch Build
yarn run watch
# Auto Restart Node Server with source code change detection.
yarn run dev
TypeScript compilation errors will be notified in Terminal.
Create src/models/framework/index.ts .
The way of defining Model depends on sequelize.
/* src/models/framework/index.ts */
import Sequelize from 'sequelize';
import { sequelizeInstance } from 'config/database';
export const FrameworksTable = {
name: 'frameworks',
schema: {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
name: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
language: {
allowNull: false,
type: Sequelize.STRING,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
},
};
export const Framework = sequelizeInstance.define(
FrameworksTable.name,
FrameworksTable.schema,
);
Create src/scripts/migrations/createFrameworks.ts . This template doesn't use sequelize/cli. preferring to more flexible scripts.
/* src/scripts/migrations/createFrameworks.ts */
import { Framework } from 'models/framework';
export const createFrameworkMigrate = () =>
new Promise(async (resolve, reject) => {
try {
await Framework.sync();
resolve();
} catch (e) {
reject(e);
}
});
Define call of the script in src/scripts/migrations.ts .
/* src/scripts/migrations.ts */
/* Add the line */
import { createFrameworkMigrate } from './migrations/createFrameworks';
(async () => {
...
/* Add the line */
await createFrameworkMigrate();
...
sequelizeInstance.close();
})();
Create src/scripts/seeds/frameworks.ts put seed data into frameworks table.
/* src/scripts/seeds/frameworks.ts */
import { Framework } from 'models/framework';
export const seedFrameworks = () =>
new Promise(async (resolve, reject) => {
await Framework.bulkCreate([
{
name: 'Express',
language: 'JavaScript',
},
{
name: 'Ruby on Rails',
language: 'Ruby',
},
{
name: 'Django',
language: 'Python',
},
{
name: 'Laravel',
language: 'PHP',
},
]).catch(e => {
/* Errorを出力する。 */
console.log(e);
});
resolve();
});
Define call of the script above in the src/scripts/seeds.ts.
/* src/scripts/seeds.ts */
/* Add the line */
import { seedFrameworks } from './seeds/frameworks';
(async () => {
...
/* Add the line */
await seedFrameworks();
sequelizeInstance.close();
})();
Creating database and migrations and putting seed can be done with executing the following commands.
yarn run db:create
yarn run db:migrate
yarn run db:seed
Create src/spec/factories/frameworkFactory.ts .
This template adopts Mocha as test framework and power-assert as a assertion library.
/* src/spec/factories/frameworkFactory.ts */
import { Framework } from 'models/framework';
export const TEST_FRAMEWORK = 'GreatFramework';
export const TEST_LANGUAGE = 'whiteSpace';
export const destroyTestFramework = () =>
new Promise(async resolve => {
await Framework.destroy({
where: {
name: TEST_FRAMEWORK,
},
});
resolve();
});
export const findOrCreateTestFramework = (otherAttrs: any) =>
new Promise(async resolve => {
const instance = await Framework.findOrCreate({
where: {
name: TEST_FRAMEWORK,
language: TEST_LANGUAGE,
},
defaults: otherAttrs,
});
resolve(instance);
});
Then write src/models/framework/spec.ts . We put a spec file into the directory in which the test target is.
import { Framework } from 'models/framework';
import assert from 'power-assert';
import {
destroyTestFramework,
findOrCreateTestFramework,
TEST_FRAMEWORK,
} from 'spec/factories/frameworkFactory';
describe('Framework', () => {
describe('Positive', () => {
beforeEach(() =>
new Promise(async resolve => {
await findOrCreateTestFramework({});
resolve();
}));
afterEach(() =>
new Promise(async resolve => {
await destroyTestFramework();
resolve();
}));
it('success', () =>
new Promise(async (resolve, reject) => {
const framework = (await Framework.findOne({
where: { name: TEST_FRAMEWORK },
})) as any;
assert.equal(framework.name, TEST_FRAMEWORK);
resolve();
}));
});
describe('Negative', () => {
it('fail without language', () =>
new Promise(async (resolve, reject) => {
try {
await Framework.create({
name: 'foobarFramework',
});
} catch (e) {
resolve();
}
}));
});
});
And testing can be done with the commands below.
yarn run db:create:test
yarn run db:migrate:test
yarn run db:seed:test
yarn run test
We check the Framework Model can be created successfully and exception are thrown when invalid Framework is tried to be created.
Let's define the action with which a user can fetch all frameworks.
Create src/controllers/api/v1/frameworks.ts.
We follow routing conventions of Ruby on Rails.
The table below is cited from https://guides.rubyonrails.org/routing.html
HTTP Verb | Path | Controller#Action | Used for |
---|---|---|---|
GET | /photos | photos#index | display a list of all photos |
GET | /photos/new | photos#new | return an HTML form for creating a new photo |
POST | /photos | photos#create | create a new photo |
GET | /photos/:id | photos#show | display a specific photo |
GET | /photos/:id/edit | photos#edit | return an HTML form for editing a photo |
PATCH/PUT | /photos/:id | photos#update | update a specific photo |
DELETE | /photos/:id | photos#destroy | delete a specific photo |
import { Request, Response } from 'express';
import { respondWith } from 'helpers/response';
import { Framework } from 'models/framework';
export const frameworksIndex = async (req: Request, res: Response) => {
try {
const frameworks = await Framework.findAll();
respondWith(res, 200, frameworks);
} catch (e) {
respondWith(res, 500);
}
};
Add a new path to src/config/path.ts . All paths referenced in the application should be defined in this file.
/* src/config/path.ts */
export const path = {
...
/* Add the line */
frameworks: '/frameworks/'
};
Add a route definition to the defineRoutes() function in config/routes.ts. All routes in the app should be defined in this file.
import { frameworksIndex } from 'controllers/api/v1/frameworks';
export const defineRoutes = (app: Express) => {
...
/* Add the line */
app.get(path.frameworks, frameworksIndex);
...
};
If you want to show the contents only to authenticated users, apply requireAuth() middleware function before frameworksIndex handler.
Let's consume the defined route with curl command.
curl -X GET http://localhost:3000/frameworks
Then, frameworks data will be returned which was put by seed as following.
{"data":
[
{"id":1,"name":"Express","language":"JavaScript"},
{"id":2,"name":"Ruby on Rails","language":"Ruby"},
{"id":3,"name":"Django","language":"Python"},
{"id":4,"name":"Laravel","language":"PHP"}
],"
status":200
}
- src/controllers/api/v1/frameworks/spec.ts
/* src/controllers/api/v1/frameworks/spec.ts */
import assert from 'power-assert';
import request from 'supertest';
import { path } from 'config/path';
import { app } from 'index';
import {
destroyTestFramework,
findOrCreateTestFramework,
TEST_FRAMEWORK,
} from 'spec/factories/frameworkFactory';
describe(`Framework Controller`, () => {
beforeEach(() =>
new Promise(async resolve => {
await findOrCreateTestFramework({});
resolve();
}));
afterEach(() =>
new Promise(async resolve => {
await destroyTestFramework();
resolve();
}));
describe('Create', () => {
describe(`Positive`, () =>
it('User will be successfully created', () =>
new Promise(resolve => {
request(app)
.get(path.frameworks)
.set('Accept', 'application/json')
.then(async (res: any) => {
const framework = res.body.data.filter(
(elem: any) => elem.name === TEST_FRAMEWORK,
);
assert.equal(framework[0].name, TEST_FRAMEWORK);
resolve();
});
})));
});
});
Testing can be done with the following command.
yarn run test