Loading
김정환
This is a live streamed presentation. You will automatically follow the presenter and see the slide they're currently on.
V8
Non-blocking Event I/O
CommonJS
ES6 support (node.green)
// sum.js
const sum = (a, b) => a + b;
module.exports = sum;
// app.js
const sum = require('./sum');
sum(1, 2); // 3
명령어
node
node
node app.js
npm
npm start
npm test
npm run [script name]
// package.json
{
"name": "apiserver",
"scripts": {
"start": "node bin/www",
"test": "NODE_ENV=test node_modules/.bin/mocha app/**/*.spec.js"
},
"dependencies": {
"express": "^4.14.0"
},
"devDependencies": {
"mocha": "^3.1.2"
}
}
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(`Run at http://${hostname}:${port}/`);
});
Web framework for nodejs
Application
Middleware
Request
Response
Router
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World!\n')
});
app.listen(3000, () => {
console.log(`Run at http://localhost:3000`)
});
const express = require('express');
const app = express();
app.use(require('morgan')('dev'));
app.get('/', (req, res) => res.send('Hello World!\n'));
app.listen(3000, () => console.log('Run at http://localhost:3000'));
app.get('/:id', (req, res) => {
const id = req.params.id; // /1
const limit = req.query.limit; // ?limit=10
const body = req.body; // {name: 'chris'}
});
app.get('/:id', (req, res) => {
res.status(204).end();
res.send('hello world');
res.json([{msg: 'hello world'}]);
});
const user = require('./user');
app.use('/users', user)
// user.js
const router = require('express').Router()
router.get('/', (req, res) => res.send('user list'));
router.post('/', (req, res) => res.send('created user'));
module.exports = router;
const app = require('express')();
app.use((req, res, next) => {
console.log('middleware 1');
next();
});
app.use((req, res, next) => {
console.log('middleware 2');
next();
});
app.get('/', (req, res, next) => {
console.log('middleware 3');
next();
}, (req, res) => {
console.log('router GET /');
res.send('hello world\n')
});
app.listen(3000, _=> console.log('Run server'));
middleware 1
middleware 2
middleware 3
router Get /
const app = require('express')();
app.use((req, res, next) => {
console.log('middleware 1');
next('error 1');
});
app.use((error, req, res, next) => {
console.log('middleware 2', error);
next();
});
app.get('/', (req, res, next) => {
console.log('middleware 3');
next();
}, (req, res) => {
console.log('router GET /');
res.send('hello world\n')
});
app.listen(3000, _=> console.log('Run server'));
middleware 1
middleware 2, error1
middleware 3
router Get /
const app = require('express')();
app.use((req, res, next) => {
console.log('middleware 1');
next('error 1');
});
app.use((error, req, res, next) => {
console.log('middleware 2', error);
next(error);
});
app.get('/', (req, res, next) => {
console.log('middleware 3');
next();
}, (req, res) => {
console.log('router GET /');
res.send('hello world\n')
});
app.listen(3000, _=> console.log('Run server'));
middleware 1
middleware 2, error1
error 1
app.use((req,res,next)=> next())
app.use((err, req,res,next)=> next())
GET /usres/:id
POST /usres
PUT /usres/:id
DELETE /usres/:id
const express = require('express');
const logger = require('morgan');
// 익스프레스 객체 생성
const app = express()
// 유저 데이터
let users = [
{id: 1, name: 'alice'},
{id: 2, name: 'bek'},
{id: 3, name: 'chris'}
];
// 로거 설정
app.use(logger('dev'));
// 요청 대기 상태 진입
app.listen(3000, () => console.log('Listening on port 3000'));
// GET /users API 구현
app.get('/users', (req, res) => res.json(users));
// GET /users/:id API 구현
app.get('/users/:id', (req, res) => {
const user = users.filter(u => u.id == req.params.id)[0];
if (user) return res.json(user);
// 사용자가 없는 경우 404 상태코드 응답
res.status(404).end();
});
// 바디 파서 미들웨어 추가
const bodyParser = require('body-parser');
// 바디 파서 미들웨어 설정
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
// post /users API 구현
app.post('/users', (req, res) => {
if (!req.body.name) return res.status(400).end();
const user = {id: Date.now(), name: req.body.name}
users.push(user);
res.status(201).json(user);
});
app.put('/users/:id', (req, res) => {
// 파라매터 에러 처리 (400 반환)
if (!req.body.name) return res.status(400).end();
const user = users.filter(u => u.id == req.params.id)[0];
// 없는 유저일 경우 에러처리 (404 반환)
if (!user) return res.status(404).end();
user.name = req.body.name;
res.json(user);
});
app.delete('/users/:id', (req, res) => {
users = users.filter(u => u.id != req.params.id);
res.status(204).end();
});
app.use('/users', require('./api/user'));
// api/user/index.js
// 라우터 객체 생성
const router = require('express').Router();
let users = [
{id: 1, name: 'alice'},
{id: 2, name: 'bek'},
{id: 3, name: 'chris'}
];
router.get('', (req, res) => res.json(users));
router.get('/:id', (req, res) => {
const user = users.filter(u => u.id == req.params.id)[0];
if (user) return res.json(user);
res.status(404).end();
});
router.post('', (req, res) => {
if (!req.body.name) return res.status(400).end();
const user = {id: Date.now(), name: req.body.name}
users.push(user);
res.status(201).json(user);
});
router.put('/:id', (req, res) => {
if (!req.body.name) return res.status(400).end();
const user = users.filter(u => u.id == req.params.id)[0];
if (!user) return res.status(404).end();
user.name = req.body.name;
res.json(user);
});
router.delete('/:id', (req, res) => {
users = users.filter(u => u.id != req.params.id);
res.status(204).end();
});
// 라우터를 외부로 노출
module.exports = router;
// api/user/index.js
// 컨트롤러로 비지니스 로직 분리
const ctrl = require('./user.ctrl');
router.get('', ctrl.index);
router.get('/:id', ctrl.show);
router.post('', ctrl.create);
router.put('/:id', ctrl.update);
router.delete('/:id', ctrl.destory);
// api/user/user.ctrl.js
let users = [
{id: 1, name: 'alice'},
{id: 2, name: 'bek'},
{id: 3, name: 'chris'}
];
const index = (req, res) => res.json(users);
const show = (req, res) => {
const user = users.filter(u => u.id == req.params.id)[0];
if (user) return res.json(user);
res.status(404).end();
};
const create = (req, res) => {
if (!req.body.name) return res.status(400).end();
const user = {id: Date.now(), name: req.body.name}
users.push(user);
res.status(201).json(user);
};
const update = (req, res) => {
if (!req.body.name) return res.status(400).end();
const user = users.filter(u => u.id == req.params.id)[0];
if (!user) return res.status(404).end();
user.name = req.body.name;
res.json(user);
};
const destory = (req, res) => {
users = users.filter(u => u.id != req.params.id);
res.status(204).end();
};
module.exports = {index, show, create, update, destory};
데이터베이스 연동
Sequelize: nodejs에서 사용하는 sql 디비용 ORM
sqlite: 개발, 테스용 / mysql: 운영용
// models/index.js
const Sequelize = require('sequelize');
// 데이터베이스 연결
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'db.development.sqlite'
});
// 유저 모델링
const User = sequelize.define('user', {
name: Sequelize.STRING,
});
module.exports = {sequelize, User};
// index.js
// 디비 초기화
models.sequelize.sync({force: true}).then(() => {
console.log('database is synced');
app.listen(3000, () => console.log('Listening on port 3000'));
});
const models = require('../../models');
const index = (req, res) => {
models.User.findAll().then(users => res.json(users));
}
const show = (req, res) => {
models.User.findOne({where: {id: req.params.id}}).then(user => {
if (!user) return res.status(404).end();
res.json(user);
});
};
const create = (req, res) => {
if (!req.body.name) return res.status(400).end();
models.User.create({name: req.body.name}).then(user => {
res.status(201).json(user);
});
};
const update = (req, res) => {
if (!req.body.name) return res.status(400).end();
models.User.findOne({where: {id: req.params.id}}).then(user => {
if (!user) return res.status(404).end();
user.name = req.body.name;
user.save().then(_ => res.json(user));
});
};
const destory = (req, res) => {
models.User.destroy({where: {id: req.params.id}}).then(() => {
res.status(204).end();
});
};
module.exports = {index, show, create, update, destory};
findAll(), findOne(), create(), update(), destroy()
Postman
Mocha, should, supertest
환경의 분리
describe('test suite', () => {
before('setup mokup', () => ...);
after('tear down mokup', () => ...);
it('test case 1', () => ...);
it('test case 2', () => ...);
});
describe('test suite', () => {
it('test case 1', () => {
users.should.be.an.instanceOf(Array)
users.should.have.length(3);
users[0].should.have.properties('id', 'name', 'caretedAt')
users[0].should.have.property('name', 'Alice')
});
});
const app = require('./app');
const request = require('supertest');
describe('...', () => {
it('...', done => {
request(app)
.get('/users')
.expect(200)
.done((err, res) => {
if (err) throw err;
res.body.should.be.instanceOf(Array);
done()
});
});
})
// bin/www.js
const models = require('../app/models');
const app = require('../app');
models.sequelize.sync({force: true}).then(() => {
console.log('database is synced');
app.listen(3000, () => console.log('Listening on port 3000'));
});
// app/index.js
const express = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');
const app = express();
if (process.env.NODE_ENV === 'development') app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use('/users', require('./api/user'));
// for test
module.exports = app;
// config/index.js
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const config = {
development: {
db: {
dialect: 'sqlite',
storage: 'db.development.sqlte'
}
},
test: {
db: {
dialect: 'sqlite',
storage: ':memory:',
logging: false
}
},
production: {
db: {
username: 'root',
password: 'root',
database: 'apiserver_production',
host: '127.0.0.1',
dialect: 'mysql',
logging: false
}
}
}
module.exports = config[process.env.NODE_ENV];
// models/index.js
const sequelize = new Sequelize(config.db);
const should = require('should');
const request = require('supertest');
const app = require('../../');
const models = require('../../models');
describe('User', () => {
const users = [
{name: 'alice'},
{name: 'bek'},
{name: 'chris'}
]
before('sync database', () => models.sequelize.sync({force: true}));
before('insert seed data', () => models.User.bulkCreate(users));
describe('GET /users', () => {
it('should return user array', done => {
request(app)
.get('/users')
.expect(200)
.end((err, res) => {
if (err) throw err;
res.body.should.be.instanceOf(Array).with.length(users.length)
done();
});
});
});
describe('GET /users/:id', () => {
it('should return user object', done => {
request(app)
.get('/users/1')
.expect(200)
.end((err, res) => {
if (err) throw err;
res.body.should.have.property('name', users[0].name)
done();
});
});
});
describe('POST /users', () => {
it('should return new user', done => {
request(app)
.post('/users')
.send({name: 'daniel'})
.expect(201)
.end((err, res) => {
if (err) throw err;
res.body.should.have.property('name', 'daniel')
done();
});
});
});
describe('PUT /users/:id', () => {
it('should update username', done => {
request(app)
.put('/users/4')
.send({name: 'david'})
.expect(200)
.end((err, res) => {
if (err) throw err;
res.body.should.have.property('name', 'david')
done();
});
});
});
describe('DELETE /users/:id', () => {
it('should delete user by id', done => {
request(app)
.delete('/users/4')
.expect(204)
.end(done);
});
});
});
"scripts": {
"start": "node bin/www",
"test": "NODE_ENV=test node_modules/.bin/mocha app/api/user/user.spec.js"
},
Github wiki + postman
apidoc + postman
swagger
스웨거 스펙을 이용해 문서 작성
const paths = {
'/users': {
get: {
tags: ['User'],
summary: 'Get users',
operationId: 'userUsers',
produces: ['application/json'],
parameters: [
],
responses: {
200: {
description: 'Success',
schema: {
type: 'array',
items: {'$ref': '#/definitions/User'}
}
}
}
},
post: {
tags: ['User'],
summary: 'Create users',
operationId: 'postUsers',
produces: ['application/json'],
parameters: [{$ref: '#/parameters/User'}],
responses: {
201: {
description: 'Created',
schema: {"$ref": "#/definitions/User"}
},
400: {description: 'BadRequest'}
}
}
}
}
const parameters = {
User: {
name: 'user',
in: 'body',
required: true,
schema: {
type: 'object',
properties: {
name: {type: 'string', default: 'chris'}
}
}
}
}
const definitions = {
User: {
type: 'object',
properties: {
id: {type: 'number'},
name: {type: 'string'},
createdAt: {type: 'string', format: 'dateTime'},
updatedAt: {type: 'string', format: 'dateTime'}
}
}
};
module.exports = {
"swagger": "2.0",
"info": {
"title": "apiserver",
"description": "apiserver API Documents",
"termsOfService": "",
"contact": {
"name": "Chris, WePlanet"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0.0"
},
"host": "",
"basePath": "/",
"schemes": [
"http"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
paths: paths,
parameters: parameters,
definitions: definitions
};
작성한 문서 호스팅
// config/swagger/index.js
const express = require('express');
const path = require('path')
const setupDocument = app => {
app.get('/swagger/doc', (req, res) => {
const doc = require('./v1.doc');
res.json(doc);
});
};
module.exports = app => {
setupDocument(app);
}
문서를 렌더링할 swagger-ui 툴을 프로젝트에 추가
// config/swagger/index.js
const setupSwaggerUi = app => {
app.use('/swagger', (req, res, next) => {
if (req.url === '/') res.redirect('/swagger?url=doc');
else next();
}, express.static(path.join(__dirname, '../../../node_modules/swagger-ui/dist')));
}
module.exports = app => {
setupDocument(app);
setupSwaggerUi(app);
}
// index.js
// 스웨거 설정
const swagger = require('./config/swagger');
swagger(app);
# hooks/post-receive
#!/bin/bash
APP_NAME=apiserver
APP_DIR=$HOME/$APP_NAME
REVISION=$(expr substr $(git rev-parse --verify HEAD) 1 7)
GIT_WORK_TREE=$APP_DIR git checkout -f
cd $APP_DIR
forever stop apiserver
forever start --uid apiserver npm bin/www