Node API 서버 만들기

NodeJS에 대해

브라우져 안의 JS

  • V8 (Chrome)
  • Webkit (Safari)
  • SpikerMonky (Firefox)

브라우져 밖의 JS

  • V8
  • Event I/O
  • CommonJS

NodeJS로 할 수 있는 것

  • Network Programming
  • Development Tool
  • Desktop Application
  • Mobile Application
  • ...

API Server

  • NodeJS
  • ExpressJS
  • REST API

Hello world!

https://nodejs.org/ko/

node --version

npm --version

npm init

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

https://nodejs.org/dist/latest-v6.x/docs/api/synopsis.html

node app.js

{
  "scripts": {
    "start": "node app.js"
  }
}

package.json

npm start

브라우져로 127.0.0.1:3000 접속

curl -X GET '127.0.0.1:3000'

curl -X GET '127.0.0.1:3000' -v

git checkout helloWorld

Hello world 코드 살펴보기

const

function foo() {
  if (false) {
    var a = 1;
  }
  console.log(a);
}

foo(); // undefined

function scope

function foo() {
  var a;
  if (false) {
    a = 1;
  }
  console.log(a);
}

foo(); // undefined
function foo() {
  if (false) {
    let a = 1;
  }
  console.log(a);
}

// ReferenceError: 
// a is not defined
foo(); 

block scope

function foo() {
  if (false) {
    let a;
    a = 1;
  }
  console.log(a);
}

// ReferenceError: 
// a is not defined
foo(); 
const a = 1;

// TypeError: 
// Assignment to constant variable.
a = 2; 

// SyntaxError: 
// Missing initializer in const declaration
const b; 

const

var
let
const

Arrow Function

var foo = function(param) {
  return true;
}

const foo = (param) => {
  return true;
}

const foo = param => true;

require()

foo.js

bar.js

require('foo')

function sum(a, b) {
  return a + b;
}

// sum()을 외부 노출 -> 모듈로 만듬
module.exports = sum;

sum.js

const sum = require('./sum.js');

console.log(sum(1, 2)); // 3

app.js

ExpressJS

ExpressJS를 사용하는 이유

HTTP

client

Server

웹 프레임웍

적은 코드로 많은 일을

npm i express --save

Express Hello world!

익스프레스 주요 개념

  1. Application
  2. Request
  3. Response
  4. Router

1. Application

  • 익스프레스 객체
  • 미들웨어 설정
  • 요청 대기 상태
  • (간단한) 라우팅

2. Request

  • req.params
  • req.query
  • req.body

3. Response

  • res.send()
  • res.json()
  • res.status()

4. Router

  • Router()
  • 구조적인 라우팅 구현
  • API 서버의 핵심 로직

REST API

요청

사용자 목록을 조회하는 요청

/getUsers

/users

/users/1

/users?limit=5&offset=10

GET /users?limit=&offset

GET /users/1

CRUD Method
Create POST
Read GET
Update PUT
Delete DELETE

응답

사용자 목록을 응답

헤더

Status Code

Content-Type: text/json

Content-Length: 13

...

바디

배열이나 객체 
[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bek' }, { id: 3, name: 'Chris' }]

2xx
4xx
5xx

상태 코드 의미
200 Success
201 Created
204 No Content
상태 코드 의미
400 Bad Request
401 Unauthorized
404 Not Found
409 Conflict
상태 코드 의미
500 Internel Error

HTTP

Query

Server

DB

Client

GET /users

GET /users/:id

POST /users

DELETE /users/:id

PUT /users/:id

GET /users

전체 사용자 목록 조회

임시 데이터

let users = [
  {
    id: 1,
    name: 'alice'
  },
  {
    id: 2,
    name: 'bek'
  },
  {
    id: 3,
    name: 'chris'
  }
]

라우팅 설정

app.get('/users', (req, res) => {

  // 여기에 라우팅 로직을 작성하면 됩니다.

});

JSON

  • 데이터 표현 방법 

  • AJAX 에서 주로 사용 

  • 자바스크립트 객체와 유사 

 res.json(users);

curl -X GET '127.0.0.1:3000/users' -v

git checkout getUsers

GET /users/:id

아이디로 사용자 조회

GET /users/1

app.get('/users/1', (req, res) => /* ... */);

GET /users/2

app.get('/users/2', (req, res) => /* ... */);

GET /users/:id

app.get('/users/:id', (req, res) => {
  console.log(req.params.id); 
});

아이디 값 처리

app.get('/users/:id', (req, res) => {
  const id = parseInt(req.params.id, 10);
});

파라매터 검증

app.get('/users/:id', (req, res) => {
  const id = parseInt(req.params.id, 10);
  if (!id) {
    return res.status(400)
        .json({error: 'Incorrect id'});
  }
});

함수 체이닝

function User(_name) {
  this.name = _name;
}

User.prototype.greeting = function() {
  console.log('Hello! ');
  return this;
};

User.prototype.introduce = function() {
  console.log(`I am ${this.name}`);
  return this;
};

var chris = new User('chris');
chris.greeting().introduce();
let user = users.filter(user => {
  return user.id === id;
});

console.log(user); // [{id: 1, name: 'alice'}]

Array.prototype.filter()

app.get('/users/:id', (req, res) => {
  let user = users.filter(user => user.id === id)[0]

  if (!user) {
    return res.status(404)
        .json({error: 'Unknown user'});
  }
});

404 에러 처리

app.get('/users/:id', (req, res) => {
  // return res.status(200).json(user);
  // or 

  return res.json(user);
});

200 성공 응답

curl -X GET '127.0.0.1:3000/users/1' -v

curl -X GET '127.0.0.1:3000/users/4' -v

curl -X GET '127.0.0.1:3000/users/alice' -v

git ckeckout getUserById

DELETE /users/:id

사용자 삭제

app.delete('/users/:id',

배열에서 유저 삭제

Array.prototype.findIndex()

Array.prototype.splice()

const userIdx = users.findIndex(user => {
    return user.id === id;
});

findIndex()

if (userIdx === -1) {
    return res.status(404)
        .json({error: 'Unknown user'});
}

404 에러 처리

users.splice(userIdx, 1);

응답

방법 1: 전체 유저 배열을 응답

방법 2: 204. No Content

res.status(204).send();

curl -X DELETE '127.0.0.1:3000/users/1' -v

curl -X GET '127.0.0.1:3000/users/1' -v

curl -X GET '127.0.0.1:3000/users' -v

git checkout deleteUserById

POST /users

사용자 추가

app.post('/users',

요청 데이터를 보내는 방법

쿼리문자열 (Query string)

바디 (Body)

https://www.google.co.kr/newwindow&q=chris

바디(body)는?

body-parser!

npm i body-parser --save

const name = req.body.name || "";

400 에러 처리

if (!name.length) {
  return res.status(400)
      .json({error: 'Incorrenct name'});
}

새로운 아이디 만들기

Array.prototype.reduce()

배열에 새로운 유저 추가

const newUser = {
  id: id,
  name: name
};

users.push(newUser);

201 응답

return res.status(201).json(newUser);

 

curl -X POST '127.0.0.1:3000/users' -d "name=daniel" -v

 

git checkout postUser

git checkout putUserById

익스프레스 Router

const express = require('express');
const router = express.Router();

 

router.get('/users', (req, res) => {
  // ...
})

 

module.exports = router;

api/users/index.js

const router = express.Router();

// Database Mockup
let users = [/* ... */];

router.get('/:id', (req, res) => {/* ... */});

router.get('/', (req, res) => {/* ... */});

router.delete('/:id', (req, res) => {/* ... */});

router.post('/:id', (req, res) => {/* ... */});

module.exports = router;

app.use('/users', require('./api/user'));

git checkout router1

api/user/user.controller.js

// Database Mockup
let users = [/* ... */];

exports.index = (req, res) => {/* ... */});

exports.get = (req, res) => {/* ... */});

exports.destroy = (req, res) => {/* ... */});

exports.create = (req, res) => {/* ... */});
const router = express.Router();
const ctrl = require('./user.controller');

router.get('/:id', ctrl.show);
router.get('/', ctrl.index);
router.delete('/:id', ctrl.destroy);
router.post('/:id', ctrl.create);

module.exports = router;

app.js: 익스프레스로 서버 설정 및 구동
api/user/index.js: User API에 대한 라우팅 설정
api/user/user.controller.js: User API에 대한 로직

git checkout router2

테스트

Mocha
Should
SuperTest

1. Mocha

npm i mocha --save-dev

Test suite: describe()

Test: it()

api/user/user.spec.js

const assert = require('assert');

describe('test suite', () => {
  it('true is true', (done) => {
    assert.equal(true, true);
  });
});

node_modules/.bin/mocha api/user/user.spec.js

{
  "scripts": {
    "test": "node_modules/.bin/mocha api/user/user.spec.js"
  }
}

2. Should

npm i should --save-dev

const should = require('should');

describe('test suite', () => {
  it('true is true', (done) => {
    (true).should.be.euqal(true);
  });
});

3. SuperTest

npm i supertest --save-dev

module.exports = app;

const request = require('supertest');
const app = require('../../app');

describe('GET /users', () => {
  it('should return 200 status code', (done) => {
    request(app)
        .get('/users')
        .expect(200)
        .end((err, res) => {
          if (err) throw err;

          console.log(res.body); 
            // [{id: 1, name: 'Alice'} ... ]

          done();
        })
  });
});
res.body.should.be.an.instanceof(Array)
    .and.have.length(3);

res.body.map(user => {
  user.should.have.properties('id', 'name');
  user.id.should.be.a.Number();
  user.name.should.be.a.String();
});

git checkout unitTest

git checkout unitTest1

git checkout unitTest2

git checkout unitTest3

git checkout unitTest4

데이터베이스

HTTP

Query

Server

DB

Client

mysql -u root -h localhost

mysql> CREATE DATABASE node_api_codlab;

mysql> USE node_api_codlab;

mysql> SHOW TABLES;

ORM

(Object Relational Mapping)

Table1

Table2

Object1

{
  id: 1
  name: 1
}

Object2

{
  id: 1
  name: 1
}

Object3

{
  id: 1
  name: 1
}

Table3

Sequelize (ORM)

npm i sequelize mysql --save

모델(Model)

models.js

const Sequelize = require('sequelize');
const sequelize = new Sequelize('node_api_codelab', 'root', 'root')

const User = sequelize.define('user', {
  name: Sequelize.STRING
});

module.exports = {
  sequelize: sequelize,
  User: User
}

데이터베이스 동기화

app.listen(3000, () => {
  console.log('Example app listening on port 3000!');

  require('./models').sequelize.sync({force: true})
      .then(() => {
        console.log('Databases sync');
      });
});

mysql> SHOW TABLES;

mysql> describe users;

git checkout sequelizeModel

컨트롤러에 디비 연동하기

const models = require('../../models');

exports.create = (req, res) => {
  const name = req.body.name || '';
  if (!name.length) {
    return res.status(400).json({error: 'Incorrenct name'});
  }

  models.User.create({
    name: name
  }).then((user) => res.status(201).json(user))
};

create()

exports.index = (req, res) => {
  models.User.findAll()
      .then(users => res.json(users));
};

findAll()

exports.show = (req, res) => {
  models.User.findOne({
    where: {
      id: id
    }
  }).then(user => {
    if (!user) {
      return res.status(404).json({error: 'No User'});
    }

    return res.json(user);
  });
};

findOne()

exports.destroy = (req, res) => {
  models.User.destroy({
    where: {
      id: id
    }
  }).then(() => res.status(204).send());
};

destroy()

git checkout sequelizeInCtrl

환경의 분리

process.env.NODE_ENV

development

test

production

config/environment.js

const environments = {
  development: {
    mysql: {
      username: 'root',
      password: 'root',
      database: 'node_api_codelab_dev'
    }
  },

  test: {
    mysql: {
      username: 'root',
      password: 'root',
      database: 'node_api_codelab_test'
    }
  },

  production: {

  }
}

const nodeEnv = process.env.NODE_ENV || 'development';

module.exports = environments[nodeEnv];

노드 환경에 따라 디비 연결하기

const Sequelize = require('sequelize');
const config = require('./config/environment');

const sequelize = new Sequelize(
    config.mysql.database, 
    config.mysql.username, 
    config.mysql.password);

git checkout env

테스트에 디비 연동하기

bin/sync-databse.js

const models = require('../models');

module.exports = () => {
  return models.sequelize.sync({force: true})
}

bin/www.js

const app = require('../app');
const syncDatabase = require('./sync-database');

app.listen(3000, () => {
  console.log('Example app listening on port 3000!');

  syncDatabase().then(() => {
    console.log('Database sync');
  });
});

package.json

{
  "scripts": {
    "start": "node bin/www"
  }
}

테스트 데이터 추가

before('sync database', (done) => {
    syncDatabase().then(() => done());
});

before('insert seed user data', (done) => {
    models.User.bulkCreate(users).then(() => done());
})

테스트 데이터 삭제

after('delete seed user data', (done) => {
  models.User.destroy({
    where: {
      name: {
        in: users.map(user => user.name)
      }
    }
  });
});

NODE_ENV=test ./node_modules/.bin/mocha

api/**/*.spec.js

package.json

{
  "scripts": {
    "test": "NODE_ENV=test node_modules/.bin/mocha api/user/user.spec.js"
  }
}

git checkout unitTestWithDb

폴더 정리

/app: 서버 기능
    /api: api 로직을 담당
    /config: 서버가 구동하기 위한 환경 변수 정의
    /models: 데이터베이스 모델링
/bin: 서버 구동을 위한 코드
    /www.js: 서버 구동
    /sync-database: 디비 싱크

git checkout refoldering

실습

PUT /users/:id를 만들어 보세요

Hints

  • router.put()
  • models.User.update()
  • models.User.findOne()
  • 400, 404, 200

[웹프레임웍스] Node API 서버 만들기

By 김정환

[웹프레임웍스] Node API 서버 만들기

NodeJS를 이용한 모바일 API 서버 만들기 입니다.

  • 5,114