
API(Application Programming Interface)
: 다른 애플리케이션에서 현재 프로그램의 기능을 사용할 수 있게 허용하는 접점
API 기능 장점
웹 사이트는 제공하고 싶은 정보만 API를 열어두고, 제공하고 싶지 않은 부분은 막아둘 수 있음
사용자는 신뢰성 있고 보증된 정보를 쉽게 수집할 수 있음
NodeBird 서비스의 post, hashtag, user를 json 형식으로 제공
특정 도메인에서만 제공된 api를 받을 수 있도록 설정
=> key도 다 직접 생성


const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const passport = require('passport');
const morgan = require('morgan');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
dotenv.config();
const authRouter = require('./routes/auth');
const indexRouter = require('./routes');
const v1Router = require('./routes/v1');
const { sequelize } = require('./models');
const passportConfig = require('./passport');
const app = express();
passportConfig();
app.set('port', process.env.PORT || 8002);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
sequelize.sync({ force: false })
.then(() => {
console.log('데이터베이스 연결 성공');
})
.catch((err) => {
console.error(err);
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
}));
app.use(passport.initialize());
app.use(passport.session());
app.use('/auth', authRouter);
app.use('/', indexRouter);
app.use('/v1', v1Router);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기중');
});
도메인을 등록하는 기능 추가 => 도메인 모델 추가
const Sequelize = require('sequelize');
module.exports = class Domain extends Sequelize.Model {
static init(sequelize) {
return super.init({
host: {
type: Sequelize.STRING(80),
allowNull: false,
},
type: {
type: Sequelize.ENUM('free', 'premium'),
allowNull: false,
},
clientSecret: {
type: Sequelize.UUID,
allowNull: false,
},
}, {
sequelize,
timestamps: true,
paranoid: true,
modelName: 'Domain',
tableName: 'domains',
});
}
static associate(db) {
db.Domain.belongsTo(db.User);
}
};
associate에 user과 domain모델을 일대다 관계 연결해줌
(user 1명이 여러 domain 소유 가능)


![]() | ![]() |
|---|
유저의 도메인 정보 등록
const express = require('express');
const { isLoggedIn } = require('./middlewares');
const { renderLogin, createDomain } = require('../controllers');
const router = express.Router();
router.get('/', renderLogin);
router.post('/domain', isLoggedIn, createDomain);
module.exports = router;
const { User, Domain } = require('../models');
const { v4 :uuvidv4 } = require('uuid');
exports.renderLogin = async (req, res, next) => {
try {
const user = await User.findOne({ where: { id: req.user?.id || null }, include: { model: Domain } });
res.render('login', {
user,
domains: user?.Domains,
})
} catch (error) {
console.error(error);
next(error);
}
};
exports.createDomain = async (req, res, next) => {
try {
const { host, type } = req.body;
await Domain.create({
host,
type,
clientSecret: uuid.v4(),
UserId: req.user.id,
clientSecret: uuid.v4(),
});
res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
};
: JSON Web Token
JWT 토큰 구성 :

payload는 토큰의 내용물이 인코딩된 부분
=> 내용을 볼 수 있음
=> JWT에 비밀번호 등 민감한 내용 포함되지 말아야 함
JWT 토큰 장점 :
변조 불가능
JWT 토큰 단점 :
용량 큼(내용물이 포함되어 있기 때문)
npm i jsonwebtoken

const jwt = require('jsonwebtoken');
exports.isLoggedIn = (req, res, next) => { // 로그인 했는지 판단
if (req.isAuthenticated()) { //passport 통해서 로그인 했는지
next();
} else {
res.status(403).send('로그인 필요');
}
};
exports.isNotLoggedIn = (req, res, next) => { // 로그인 안 했는지 판단
if (!req.isAuthenticated()) {//passport 통해서 로그인 안했는지
next();
} else {
const message = encodeURIComponent('로그인한 상태입니다.');
res.redirect(`/?error=${message}`);
}
};
exports.verifyToken = (req, res, next) => {
try {
res.locals.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);
return next();
} catch (error) {
if(error.name === 'TokenExpiredError') {
return res.status(419).json({
code: 419,
message: '토큰이 만료되었습니다.',
});
}
return res.status(401).json({
code: 401,
message: '유효하지 않은 토큰입니다.',
});
}
}
미들웨어 정상 작동 테스트
(v1 = version 1)
const express = require('express');
const { verifyToken } = require('./middlewares');
const { createToken, tokenTest } = require('../controllers/v1');
const router = express.Router();
router.post('/token', createToken); // 토큰 발급 라우터
router.get('/test', verifyToken, tokenTest); // 토큰 테스트 라우터9
module.exports = router;
app.js에 등록해놓은 v1Router 사용
const jwt = require('jsonwebtoken');
const { Domain, User } = require('../models');
// 토큰 발급
// POST /v1/token
exports.createToken = async (req, res, next) => {
const { clientSecret } = req.body;
try {
const domain = await Domain.findOne({
where: { clientSecret },
include: {
model: User,
attributes: ['id', 'nick'],
},
});
if (!domain) {
return res.status(401).json({
code: 401,
message: '등록되지 않은 도메인입니다. 먼저 도메인을 등록하세요',
});
}
const token = jwt.sign({
id: domain.User.id,
nick: domain.User.nick,
}, process.env.JWT_SECRET, {
expiresIn: '1m',
issuer: 'nodebird',
});
return res.json({
code: 200,
message: '토큰이 발급되었습니다.',
token,
});
} catch (error) {
console.error(error);
return res.status(500).json({
code: 500,
message: '서버 에러',
});
}
};
// 토큰 테스트
// GET /v1/test
exports.testToken = (req, res) => {
res.json(res.locals.decoded);
}
API를 사용하는 서비스 생성
=> lecture-api와 같은 위치에 폴더 생성

const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
dotenv.config();
const indexRouter = require('./routes');
const app = express();
app.set('port', process.env.PORT || 4000);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
app.use(morgan('dev'));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
}));
app.use('/', indexRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기중');
});


const express = require('express');
const { test } = require('../controllers');
const router = express.Router();
router.get('/test', test);
module.exports = router;
const axios = require('axios');
exports.test = async (req, res, next) => { // 토큰 테스트 라우터
try {
if (!req.session.jwt) { // 세션에 토큰이 없으면 토큰 발급 시도
const tokenResult = await axios.post('http://localhost:8002/v1/token', {
clientSecret: process.env.CLIENT_SECRET,
});
if (tokenResult.data?.code === 200) { // 토큰 발급 성공
req.session.jwt = tokenResult.data.token; // 세션에 토큰 저장
} else { // 토큰 발급 실패
return res.status(tokenResult.data?.status).json(tokenResult.data); // 발급 실패 사유 응답
}
}
// 발급받은 토큰 테스트
const result = await axios.get('http://localhost:8002/v1/test', {
headers: { authorization: req.session.jwt },
});
return res.json(result.data);
} catch (error) {
console.error(error);
if (error.response?.status === 419) { // 토큰 만료 시
return res.json(error.response.data);
}
return next(error);
}
};
nodebird-api의 입장으로 돌아와서 나머지 완성
...
const { createToken, tokenTest, getMyPosts, getPostsByHashtag } = require('../controllers/v1');
...
router.get('/posts/my', verifyToken, (req, res) => {
Post.findAll({ where: { userId: req.decoded.id } })
.then((posts) => {
console.log(posts);
res.json({
code: 200,
payload: posts,
});
})
.catch((error) => {
console.error(error);
return res.status(500).json({
code: 500,
message: '서버 에러',
});
});
});
router.get('/posts/hashtag/:title', verifyToken, async (req, res) => {
try {
const hashtag = await Hashtag.findOne({ where: { title: req.params.title } });
if (!hashtag) {
return res.status(404).json({
code: 404,
message: '검색 결과가 없습니다',
});
}
const posts = await hashtag.getPosts();
return res.json({
code: 200,
payload: posts,
});
} catch (error) {
console.error(error);
return res.status(500).json({
code: 500,
message: '서버 에러',
});
}
});
module.exports = router;
const { Domain, User, Hashtag, Post } = require('../models');
...
// 토큰 테스트
// GET /v1/test
exports.testToken = (req, res) => { ... }
// 나의 게시글 보기
exports.getMyPosts = (req, res) => {
Post.findAll({ where: { userId: req.decoded.id } })
.then((posts) => {
res.json({
code: 200,
payload: posts,
});
})
.catch((error) => {
console.error(error);
return res.status(500).json({
code: 500,
message: '서버 에러',
})
})
}
// 해시태그 검색 결과
exports.getPostsByHashtag = async (req, res) => {
try {
const hashtag = await Hashtag.findOne({ where: { title: req.params.title } }); // req.params.title로 해시태그 검색
if (!hashtag) { // 검색결과 없음 or 쓰인적 없는 해시태그
return res.status(404).json({
code: 404,
message: '검색 결과가 없습니다',
});
}
const posts = await hashtag.getPosts(); // 해시태그로 게시글들 검색
if(posts.length === 0) {
return res.status(404).json({
code: 404,
message: '검색 결과가 없습니다',
});
}
return res.json({
code: 200,
payload: posts,
});
} catch (error) {
console.error(error);
return res.status(500).json({
code: 500,
message: '서버 에러',
});
}
}
nodecat 에서 GET /post/my 와 GET /posts/hashtag/:title 라우터 테스트
- nodecat/routes/index.js
- nodecat/controllers/index.js
- request : getMyPosts와 getPostsByHashtag에서 공통적으로 쓰이는 코드, 토큰 발급/갱신 담당
일정 기간 내에 api 사용할 수 있는 횟수 제한
-> 서버 트래픽 감소
npm i express-rate-limit
const jwt = require('jsonwebtoken');
exports.isLoggedIn = (req, res, next) => { ... };
exports.isNotLoggedIn = (req, res, next) => { ... };
exports.verifyToken = (req, res, next) => { ... }
exports.apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1분
max: 1, // 1번 허용
handler(req, res) {
return res.status(429).json({
code: this.statusCode,
message: '1분에 한 번만 요청할 수 있습니다.',
});
}
});
exports.deprecated = (req, res) => { // v2로 업그레이드 안내 미들웨어
res.status(410).json({
code: 410,
message: '새로운 버전이 나왔습니다. 새로운 버전을 사용하세요.',
});
}
API 응답 목록
새로운 V2라우터를 만든다.
controllers/v1.js에서 1분이었던 토큰 유효기간을 30분으로 수정
const express = require('express');
const { verifyToken, apiLimiter } = require('./middlewares');
const { createToken, tokenTest, getMyPosts } = require('../controllers/v1');
const router = express.Router();
// apiLimiter 추가
router.post('/token', apiLimiter,createToken);
router.get('/test', apiLimiter, verifyToken, tokenTest);
router.get('/posts/my', apiLimiter, verifyToken, getMyPosts);
router.get('/posts/hashtag/:hashtag', apiLimiter, verifyToken, getPostsByHashtag);
module.exports = router;
모든 라우터들의 앞에 deprecated 라우터 추가 -> v1으로 접근한 모든 요청에 deprecated 응답
const express = require('express');
const { verifyToken } = require('./middlewares');
const { createToken, tokenTest } = require('../controllers/v1');
const router = express.Router();
router.use(deprecated); // 미들웨어
router.post('/token', createToken);
router.get('/test', verifyToken, tokenTest);
module.exports = router;
새로 만든 v2라우터를 서버와 연결
...
const authRouter = require('./routes/auth');
const indexRouter = require('./routes');
const v1Router = require('./routes/v1');
const v2Router = require('./routes/v2'); // new!
...
BUT 이 방법이 ddos공격 방어에 딱히 효과가 있지는 않다.


이전까자는 nodeBird - nodeCat 서버끼리 api호출했음
nodeCat의 프론트에서 nodeBird를 호출하면?
=> CORS 에러 발생
요청을 보내는 클라이언트(Port 4000)와 요청을 받는 서버(Port 8002)의 도메인이 다름
=> CORS 에러 발생
에러 해결하려면 nordBird 서버에 cors 모듈을 설치하고 미들웨어 확장 패턴을 사용해 준다.
npm i cors

const jwt = require('jsonwebtoken');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const { Domain, User } = require('../models');
...
exports.corsWhenDomainMatches = async (req, res, next) => {
const domain = await Domain.findOne({
where: { host: new URL(req.get('Origin')).host }
});
if (domain) {
cors({ origin: req.get('Origin'), credentials: true })(req, res, next);
} else {
next();
}
}
