Skip to content

midoru0121/node-api-server-starter

Repository files navigation

Node Api Server Starter

日本語

Summary

Node.js API Server template based on TypeScript.

Prerequisite

  • Node.js (Version 8 LTS)
  • Yarn
  • direnv
  • MySQL

Table of Contents

Architecture

  • 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.

Directory Structure

  • 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.

Getting Started

git clone git@github.com: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

How to Develop

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.

Add New Model

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,
);

Add Migration File

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();
})();

Add Seed Data

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();
})();

Execute Migration And Put Seed Data

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

Testing Model

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.

Add Action to Controller

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 new routing definition

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.

Give it a try

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
}

Writing Controller Spec

  • 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

About

Node.js Api Server Starter with TypeScript.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published