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!
익스프레스 주요 개념
- Application
- Request
- Response
- 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
Copy of Node API 서버 만들기 (웹프레임웍스)
By BYUN Kyuhyun
Copy of Node API 서버 만들기 (웹프레임웍스)
NodeJS를 이용한 모바일 API 서버 만들기 입니다.
- 585