TIL. 100 Node.js Project_javascript
출처: 위코드 춤추는 개발자
> wecode | 위코드 | 코딩 부트캠프 | 코딩교육
WeCode | 위코드 | 코딩 부트캠프 | 코딩교육
WeCode(위코드)의 부트캠프를 통해 개발자로서 커리어를 시작하세요.
wecode.co.kr
Node.js 백엔드 API 학습자료 정리
javascript 와 Typescript가 어떻게 다른지 동일한 로직을 언어만 다르게 구성한 2가지 자료를 함께 보면
javascript를 모르더라도 Python 지식만으로도 코드를 분석할 수 있었다.
지극히 Django 와 비교하여 알아보고있는점을 참고하자.
아직까지는 Django가 편한 것이 사실이다!
javascript_version
구조
1. 모듈화
Error catch 함수 모듈화
validation Token 함수 모듈화
validation Fields 함수 모듈화
2. Django 와 비교
Project/urls.py -> routes/index.js
app/urls.py -> appRouter.js
app/views.py -> app/controller + app/service
controller -> 전체 로직 처리(검증)
service -> DB 접근하는 모든 로직 처리 (ORM : prisma)
utills.py ->
middwaresd <-- validationtoken.js
utills <-- validationFirelds.js
3. 추후 업데이트 사항 (Question)
3.1 UserRouter.js -> body를 별도로 import 하여 사용하는 이유
(Typescript 에서는 import 해서 사용하지 않음)
3.2 UserController -> Token salt를 env 파일에서 굳이 임포트해 사용하는 이유
3.3 ArticleController -> validationFields를 아래와 같이 사용하는 이유 (단지 모듈화인지?)
3.4 CommentController -> Error catch가 되어있지 않는 이유
3.5 utils -> makeDataForCreate.js // makeQueryOption.js 질문
3.6 server.js 세팅 질문
routes
UserRouter.js
const express = require('express')
const router = express.Router()
const { body } = require('express-validator')
const { UserController } = require('../controllers')
router.post(
'/login',
body('email').isEmail(),
body('password').isLength({ min: 5 }),
UserController.logIn
)
router.post(
'/signup',
body('email').isEmail(),
body('password').isLength({ min: 5 }),
UserController.signUp
)
module.exports = router
// 요청이 들어올때 Body에 대한 내용을 확인하기 위해 body를 따로 임포트함
// 유저 라우터와 유저 컨트롤러 커넥트 현재 유저 컨트롤러 안의 회원가입/로그인 함수를 불러와 사용함
// logIn/ signUp
// body에 담겨오는 패스워드의 경우 최소 5글자 이상이여야 하는것으로 보임
// body를 임포트 해와야한다는 것!
ArticleRouter.js
const express = require('express')
const router = express.Router()
const { ArticleController, CommentController } = require('../controllers')
const { validateToken } = require('../middlewares')
// articles router
router.get('/', ArticleController.getArticles)
router.get('/:articleId', ArticleController.getOneArticle)
router.post('/', validateToken, ArticleController.postOneArticle)
router.put('/:articleId', validateToken, ArticleController.updateOneArticle)
router.put('/publish/:articleId', validateToken, ArticleController.publishOneArticle)
router.delete('/:articleId', validateToken, ArticleController.deleteOneArticle)
// comments router
router.get('/:articleId/comments', CommentController.getComments)
router.post('/:articleId/comments', validateToken, CommentController.postComment)
router.put('/:articleId/comments/:commentId', validateToken, CommentController.updateComment)
router.delete('/:articleId/comments/:commentId', validateToken, CommentController.deleteComment)
module.exports = router
// 라우트 함수 임포트
// 컨트롤러의 함수를 임포트해오면서 라우트와 컨트롤러 커넥트
// 토큰 인가 임포트 ==> 토큰 인가를 미들웨어 위치시킴!
// 접근 허가 권한을 나누어 게시물CRUD URL 설정
// 댓글 URL 설정
// 여기서 핵심은 서로 관련있는 기사와 댓글을 한 군데 작성하였다는 것
Controllers
UserController.js
const { AUTH_TOKEN_SALT } = process.env
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const { UserService } = require('../services')
const { errorWrapper, errorGenerator } = require('../errors')
const signUp = errorWrapper(async (req, res) => {
const { email, password } = req.body
const hashedPassword = await bcrypt.hash(password, 10)
const foundUser = await UserService.findUser({ email })
if (foundUser) errorGenerator({ statusCode: 409, message: 'duplicated' })
const createdUser = await UserService.createUser({
email,
password: hashedPassword,
})
res.status(201).json({
message: 'user created',
email: createdUser.email,
})
})
const logIn = errorWrapper(async (req, res) => {
const { email, password: inputPassword } = req.body
const foundUser = await UserService.findUser({ email })
if (!foundUser) errorGenerator({ statusCode: 400, message: 'client input invalid' })
const { id, password: hashedPassword } = foundUser
const isValidPassword = await bcrypt.compare(inputPassword, hashedPassword)
if (!isValidPassword) errorGenerator({ statusCode: 400, message: 'client input invalid' })
const token = jwt.sign({ id }, AUTH_TOKEN_SALT)
res.status(200).json({ message: 'login success!', token })
})
module.exports = {
logIn,
signUp,
}
// 토큰 솔트 값을 따로 env 파일에서 임포트해서 사용함
// 토큰 및 암호화 임포트
// 컨트롤러와 서비스 커넥트
// 어떠한 요청에 대한 응답으로 에러가 가는 경우를 안의 로직에 사용하지 않고 모듈화로 별도의
// 에러 캐치 함수를 생성 후 임포트하여 재사용하고 있음
// (Signup과 logIn에서 사용하는 founduser는 유니크 Key 값을 가지고있음)
// SignUp
// SignUp 함수는 에러감지 함수를 인자로 받고 그 함수는 요청과 응답을 인자로 받음
// 요청 body 내용을 읽어 이메일, 비밀번호 객체로 선언
// 요청으로 들어오 비밀번호를 암호화하여 암호된 비밀번호 변수에 할당 (Q : 이때 10의 의미는?)
// 회원가입을 시도한 이메일을 DB 안에 해당 이메일이 있는지 확인함 (해당 과정은 서비스에서 담당)
// 만약 유저가 존재한다면 (값이 있다면) 에러제너레이터 함수에 코드와 메시지를 인자로 전달하여 응답
// 같은 유저가 존재하지 않는다면 이메일, 비밀번호 객체를 createUser 함수의 인자로 전달하여 회원가입 성공 메시지와 status 코드를 응답해줌
// logIn
// login 함수 처음 시작은 위와 동일
// 요청 body의 내용을 이메일과 비밀번호 객체로 선언
// 로그인을 요청한 이메일을 확인하여 DB 안에 해당 이메일을 가진 유저가 있는지 확인
// 만약 이메일이 없다면 (회원이 아닌경우) 에러제너레이터 함수에 코드와 메시지를 인자로 전달 및 응답
// 이메일이 존재한다면(회원인 경우) founduser 값의 id를 사용하여 입력된 패스워드를 암호화된 패스워드와 비교 compare 함수가 알아서 확인해주는것으로 보임
// 만약 비밀번호가 다르다면 에러 함수에 코드와 메시지를 인자로 전달 및 응답
// 비밀번호가 같다면 Salt 와 접속한 유저의 id를 이용해 jwt 토큰을 token에 선언해줌
// 메시지와 발행한 토큰함께 응답
// 여기서 에러 캐시 함수 모듈화 하였으며 장고에서와 달리 DB 히트하는 부분은 전적으로 서비스가 담당하고 있음
// 따라서 역할이 나누어져있음을 확인할 수 있음
ArticleController.js
const { ArticleService } = require('../services')
const { errorWrapper, errorGenerator } = require('../errors')
const { validateFields } = require('../utils')
const getArticles = errorWrapper(async (req, res) => {
const articles = await ArticleService.findArticles(req.query)
res.status(200).json({ articles })
})
const getOneArticle = errorWrapper(async (req, res) => {
const { articleId } = req.params
const article = await ArticleService.findArticle({ id: articleId })
if (article.deleted_at) return res.status(200).json({ message: 'deleted ' })
res.status(200).json({ article })
})
const postOneArticle = errorWrapper(async (req, res) => {
const { id: userId } = req.foundUser
const { title, body } = req.body
if (!title || !body) errorGenerator({ statusCode: 400, message: 'invalid key error' })
const createdArticle = await ArticleService.createArticle({
userId,
title,
body,
})
res.status(201).json({ createdArticle })
})
const updateOneArticle = errorWrapper(async (req, res) => {
const { id: userIdFromToken } = req.foundUser
const { articleId } = req.params
const requestedFields = req.body
const allowedFields = ['title', 'body']
const isValidFields = validateFields(requestedFields, allowedFields)
if (!isValidFields) errorGenerator({ statusCode: 400, message: 'invalid requested fields' })
const foundArticle = await ArticleService.findArticle({ id: articleId })
const { user_id: userIdFromArticle } = foundArticle
if (userIdFromToken !== userIdFromArticle)
errorGenerator({ statusCode: 403, message: 'unauthorized' })
const updatedArticle = await ArticleService.updateArticle({
articleId,
requestedFields,
})
res.status(201).json({ updatedArticle })
})
const publishOneArticle = errorWrapper(async (req, res) => {
const { id: userIdFromToken } = req.foundUser
const { articleId } = req.params
const foundArticle = await ArticleService.findArticle({ id: articleId })
if (!foundArticle) errorGenerator({ statusCode: 404, message: 'article not found' })
const { user_id: userIdFromArticle } = foundArticle
if (userIdFromToken !== userIdFromArticle)
errorGenerator({ statusCode: 403, message: 'unauthorized' })
const publishedArticle = await ArticleService.publishArticle(articleId)
res.status(201).json({ publishedArticle })
})
const deleteOneArticle = errorWrapper(async (req, res) => {
const { id: userIdFromToken } = req.foundUser
const { articleId } = req.params
const foundArticle = await ArticleService.findArticle({ id: articleId })
if (!foundArticle) errorGenerator({ statusCode: 404, message: 'article not found' })
const { user_id: userIdFromArticle } = foundArticle
if (userIdFromToken !== userIdFromArticle)
errorGenerator({ statusCode: 403, message: 'unauthorized' })
const deletedArticle = await ArticleService.deleteArticle(articleId)
res.status(201).json({ deletedArticle })
})
module.exports = {
getArticles,
getOneArticle,
postOneArticle,
updateOneArticle,
publishOneArticle,
deleteOneArticle,
}
// Q : validataefiles의 역할
// getArticles
// 함수 관련 URL 요청이 들어올 경우 서비스의 findearticles 함수의 인자로 요청 쿼리를 전달 및 응답
// getOneArticle
// 함수 관련 URL 요청(id) 들어올 경우 요청의 파라미터를 articleld 객체 저장하고 이를 이용하여
// 요청이 들어온 파라미터(id)와 같은 값을 가지고있는 기사를 article에 선언
// 만약 해당 기사가 삭제(deleted_at)된 내용이 있다면 정상 URL이지만 해당 기사는 삭제되었음을 응답
// 문제 없을 경우 json 데이터로 article 응답
// postOneArticle
// 토큰 인가에 따라 요청한 user의 id , 기사 제목 및 내용을 객체로 선언
// 만약 제목 or 내용 중 하나라도 존재하지 않을 경우 에러 응답
// 요청 값을 이용해 기사(문서) 내용 DB 저장
// 201과 함께 저장된 json 데이터로 article 응답
// updateOneArticle
// 함수 관련 URL 요청(id)으로 부터 기사의 파라미터(id)값이 필요
// 토큰 인가에 따라 요청한 user의 id 와 수정하고자할 파마티터 값을 객체로 선언
// 요청 body의 내용을 변수로 선언
// 여기서 validateFields 함수를 사용하기 위해 비교할 allowedFields를 선언함
// validateFields는 요청으로 들어온 body의 내용을 갖고 있는 reqeustFieldsr가 제목, 내용을 가지고 있는지를 판단함!!
// requestFields가 제목, 내용을 포함하고 있을 경우 ## Q 답변 ##
// 포함하고 있지 않을 경우 에러 응답
// 포함하고 있을 경우 id를 가지고 기사를 찾고 기사를 작성한 user id와 요청으로 들어온 user id가 같은지 검증
// 같지 않다면 에러 응답
// 같다면 requestFields 내용대로 해당 기사 update
// 업데이트 완료 후 201과 함께 json 데이터 변경된 art 응답
// publishOneArticle
// 올린 기사의 출판 상태 여부를 변경
// 기사를 올린 후 출판이 완료되면 스테이터스 값을 변경하게 됨
// uesr id, article id를 객체로 선언
// article id에 맞는 기사가 존재하는지 확인
// 존재하지 않을 경우 에러 응답
// 존재할 경우 기사를 올린 user id 를 객체로 선언
// 출판을 요청하는 id와 기사를 올린 id가 같은지 검증
// 다를 경우 에러 , 같을 경우 status: 'PUBLISHED'로 변경됨(서비스에서 확인 가능)
// 201과 함께 json 데이터 응답
// deleteOneArticle
// user id, article id 객체로 선언
// 각각의 id가 맞는지 검증 (올린 사람이 지울 수 있다.)
// 둘다 맞을 경우 DB에서 해당 기사 삭제
// 201과 함께 json (데이터 삭제된 기사) 응답
CommentController.js
const { CommentService } = require('../services')
const { errorWrapper, errorGenerator } = require('../errors')
const getComments = errorWrapper(async (req, res) => {
const { articleId } = req.params
const comments = await CommentService.fetchComments({ article_id: Number(articleId) })
res.status(200).json({ comments })
})
const postComment = errorWrapper(async (req, res) => {
const { articleId } = req.params
const { id: userIdFromToken } = req.foundUser
const { body } = req.body
const createdComment = await CommentService.createComment({
article_id: Number(articleId),
user_id: userIdFromToken,
body,
})
res.status(201).json({ createdComment })
})
const updateComment = errorWrapper(async (req, res) => {
const { articleId, commentId } = req.params
const { body } = req.body
const { id: userIdFromToken } = req.foundUser
const comments = await CommentService.fetchComments({ article_id: Number(articleId) })
const foundComment = comments.find((comment) => comment.id === Number(commentId))
if (!foundComment) errorGenerator({ message: 'not found', statusCode: 404 })
const isValidUser = foundComment.user_id === userIdFromToken // true or false
if (!isValidUser) errorGenerator({ message: 'unauthorized', statusCode: 403 })
const updatedComment = await CommentService.updateComment({
comment_id: Number(commentId),
body,
})
res.status(200).json({ updatedComment })
})
const deleteComment = errorWrapper(async (req, res) => {
const { articleId, commentId } = req.params
const { id: userIdFromToken } = req.foundUser
const comments = await CommentService.fetchComments({ article_id: Number(articleId) })
const foundComment = comments.find((comment) => comment.id === Number(commentId))
if (!foundComment) errorGenerator({ message: 'not found', statusCode: 404 })
const isValidUser = foundComment.user_id === userIdFromToken
if (!isValidUser) errorGenerator({ message: 'unauthorized', statusCode: 403 })
const deletedComment = await CommentService.deleteComment({
comment_id: Number(commentId),
})
res.status(200).json({ deletedComment })
})
module.exports = {
getComments,
postComment,
updateComment,
deleteComment,
}
// 당연히 컨트롤러와 서비스 커넥트
// getComments
// 함수 관련 URL 요청시 파라미터(id) 값 객체로 선언
// 해당 article에 관련된 댓글 응답 (Q : 서비스상에서 댓글 body내용을 출력해주지 않음)
// postComment
// 토큰 인가를 통한 user id, article id, 댓글 내용(요청.body)를 객체로 선언
// 토큰 인가만 문제없다면 바로 DB에 댓글 생성 (Q : Error catch가 되어있지 않음!)
// updateComment
// user id, article id 말고도 comment id값도 객체로 선언해줌
// comments 변수에 해당 기사의 댓글을 가져옴 (이때는 서비스상 atricle id와 deleted_ad 여부만 가져옴)
// 앞서 comments 값에서 req(comment id)값을 이용해 요청으로 들어온 comment id와 같은 id를 가진 댓글이 있는지 검증
// 만약 일치하는 댓글이 없다면(!False)=> (True) 에러 응답
// 일치하는 댓글이 있다면 해당 하는 댓글을 올린 작성자가 맞는지를 검증
// 다른 작성자라면 에러 응답
// 같은 작성자라면 해당 댓글 내용 업데이트
// deletecomment
// user id, article id 말고도 comment id값도 객체로 선언해줌
// update과정과 동일
// id 값이 모두 일치할 경우 해당 댓글 삭제
Services
UserService.js
const prisma = require('../prisma')
const createUser = (fields) => {
return prisma.users.create({ data: fields })
}
const findUser = (field) => {
const [uniqueKey] = Object.keys(field)
const isKeyId = uniqueKey === 'id'
const value = isKeyId ? Number(field[uniqueKey]) : field[uniqueKey]
return prisma.users.findUnique({ where: { [uniqueKey]: value } })
}
module.exports = {
createUser,
findUser,
}
// prisma ORM 사용!으로 인한 임포트
// 컨트롤러에서 회원가입시 받는 이메일과 암호화된 비밀번호를 fields 라는 인자로 받아 prisma ORM을 이용해 데이터 users 테이터 생성
// uniquekey 값이 'id' 일 경우 isKeyId = True
// isKeyId 값이 True 일 경우 value = Number(field[uniqueKey]) 선언 (불린)
// isKeyId 값이 False 일 경우 value = field[uniqueKey] 선언 (삼항연산자)
ArticleService.js
const { body } = require('express-validator')
const prisma = require('../prisma')
const { makeQueryOption } = require('../utils')
const ARTICLES_DEFAULT_OFFSET = 0
const ARTICLES_DEFAULT_LIMIT = 5
const findArticles = (query) => {
const { offset, limit, ...fields } = query
const where = makeQueryOption(fields)
return prisma.articles.findMany({
where,
skip: Number(offset) || ARTICLES_DEFAULT_OFFSET,
take: Number(limit) || ARTICLES_DEFAULT_LIMIT,
orderBy: {
created_at: 'desc',
},
})
}// pagination 발견!
const findArticle = (field) => {
const [uniqueKey] = Object.keys(field)
const isKeyId = uniqueKey === 'id'
const value = isKeyId ? Number(field[uniqueKey]) : field[uniqueKey]
return prisma.articles.findUnique({
where: { [uniqueKey]: value },
include: {
users: {
select: {
id: true,
email: true,
},
},
comments: {
where: {
deleted_at: null,
},
},
},
})
}
const createArticle = (fields) => {
const { userId: user_id, ...dataFields } = fields
return prisma.articles.create({
data: {
...dataFields,
user_id,
},
})
}
const updateArticle = (fields) => {
const { articleId, requestedFields } = fields
return prisma.articles.update({
where: {
id: Number(articleId),
},
data: {
...requestedFields,
updated_at: new Date(),
},
})
}
const publishArticle = (articleId) => {
return prisma.articles.update({
where: {
id: Number(articleId),
},
data: {
status: 'PUBLISHED',
updated_at: new Date(),
},
})
}
const deleteArticle = (articleId) => {
return prisma.articles.update({
where: {
id: Number(articleId),
},
data: {
deleted_at: new Date(),
},
})
}
module.exports = {
findArticles,
findArticle,
createArticle,
updateArticle,
publishArticle,
deleteArticle,
}
CommentService.js
const prisma = require('../prisma')
const fetchComments = ({ article_id }) => {
return prisma.comments.findMany({
where: {
article_id,
deleted_at: null,
},
})
}
const createComment = ({ article_id, user_id, body }) => {
return prisma.comments.create({
data: {
article_id,
user_id,
body,
},
})
}
const updateComment = ({ comment_id, body }) => {
return prisma.comments.update({
where: {
id: comment_id,
},
data: {
body,
},
})
}
const deleteComment = ({ comment_id }) => {
return prisma.comments.update({
where: {
id: comment_id,
},
data: {
deleted_at: new Date(),
},
})
}
module.exports = {
fetchComments,
createComment,
updateComment,
deleteComment,
}
errors
errorWrapper.js
const { validationResult } = require('express-validator')
const errorGenerator = require('./errorGenerator')
const errorWrapper = (controller) => async (req, res, next) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) errorGenerator({ statusCode: 400 })
await controller(req, res, next)
} catch (err) {
next(err)
}
}
module.exports = errorWrapper
errorGenerator.js
const DEFAULT_HTTP_STATUS_MESSAGES = {
400: 'Bad Requests',
401: 'Unauthorized',
403: 'Foribdden',
404: 'Not Found',
500: 'Internal Server Error',
503: 'Temporary Unavailable',
}
const errorGenerator = ({ message = '', statusCode = 500 }) => {
const err = new Error(message || DEFAULT_HTTP_STATUS_MESSAGES[statusCode])
err.statusCode = statusCode
throw err
}
module.exports = errorGenerator
// 만약 에러가 발생한다면
// err 변수에 statuscode에 따른 에러 메시지를 반환하고
// 응답으로 err을 전달해줌
middlewares
validationToken.js
const { errorWrapper, errorGenerator } = require('../errors')
const { UserService } = require('../services')
const { AUTH_TOKEN_SALT } = process.env
const jwt = require('jsonwebtoken')
const validateToken = errorWrapper(async (req, res, next) => {
const [bearer, token] = req.headers.authorization.split(' ')
const { id } = jwt.verify(token, AUTH_TOKEN_SALT)
const foundUser = await UserService.findUser({ id })
if (!foundUser) errorGenerator({ statusCode: 404, message: 'user not found' })
req.foundUser = foundUser
next()
})
module.exports = validateToken
utills
makeDataForCreate.js
const makeDataForCreate = (fields) =>
Object.entries(fields).reduce((data, [key, value]) => {
data[key] = value
return data
}, {})
module.exports = makeDataForCreate
makeQueryOption.js
const DEFAULT_QUERY_OPTION = {
deleted_at: null,
// status: 'PUBLISHED',
}
const getQueryOption = (key, value) => {
const mapper = {
user_id: { [key]: Number(value) },
comments: { [key]: { some: { body: { contains: value } } } },
}
const matched = mapper[key]
if (matched) return matched
return { [key]: { contains: value } }
}
const entriesAndMap = (fields, cb) => Object.entries(fields).map(cb)
const makeQueryOption = (fields) => {
if (!fields) return DEFAULT_QUERY_OPTION
const defaultQueryOptions = entriesAndMap(DEFAULT_QUERY_OPTION, ([key, value]) => ({
[key]: value,
}))
const queryOptins = entriesAndMap(fields, ([key, value]) => getQueryOption(key, value))
const where = { AND: [...defaultQueryOptions, ...queryOptins] }
return where
}
module.exports = makeQueryOption
validationFields.js
const validateFields = (requested, allowed) =>
Object.keys(requested).every((field) => allowed.includes(field))
module.exports = validateFields
app.js
const express = require('express')
const routes = require('./routes')
const logger = require('morgan')('dev')
const app = express()
app.use(express.json())
app.use(logger)
app.use(routes)
// general error handler
app.use((err, req, res, next) => {
const { statusCode, message } = err
console.error(err)
res.status(statusCode || 500).json({ message })
})
module.exports = app
Sserver.js
require('dotenv').config()
const { PORT } = process.env
const http = require('http')
const app = require('./app')
const server = http.createServer(app)
const prisma = require('./prisma')
const start = async () => {
try {
server.listen(PORT, () => console.log(`Server is listening on ${PORT}`))
} catch (err) {
console.error(err)
await prisma.$disconnect()
}
}
start()
Typescript_version
구조
1. 모듈화
Error catch 함수 모듈화 (JS와 동일)
validation Token 함수 모듈화 (JS와 동일)
Article.ts 하나로 JS에서의 utills를 포함 시킴 (JS diff)
2. Django 와 비교
동일
3. 추후 업데이트 사항 (Question)
3.1 Service에서 userCreateInput / userUniqueSearchInput 등 인스턴스를 새로 생성하여 사용하는 이유
UserController
UserService
3.2 app.js /ts && server.js/ts 코드 리뷰
3.3 Article.ts 에 대한 코드 리뷰 요청
routes
UserRouter.ts
import { Router } from 'express'
import { UserController } from '../controllers'
const router = Router()
router.use('/signup', UserController.signUp)
router.use('/login', UserController.logIn)
export default router
// JS와 달리 해당 부분에서 body를 별도로 임포트하지 않음
ArticleRouter.ts
import { Router } from 'express'
import { ArticleController } from '../controllers'
import { validateToken } from '../middlewares'
const router = Router()
router.get('/', ArticleController.getArticles)
router.get('/:article_id', ArticleController.getArticle)
router.post('/', validateToken, ArticleController.postArticle)
router.put('/:article_id', validateToken, ArticleController.updateArticle)
router.put('/publish/:article_id', validateToken, ArticleController.publishArticle)
router.delete('/:article_id', validateToken, ArticleController.deleteArticle)
export default router
Controller
UserController.ts
import bcrypt, { hash } from 'bcryptjs' // js diff
import jwt from 'jsonwebtoken'
import { UserService } from '../services'
import { errorWrapper, errorGenerator} from '../errors'
import { Request, Response } from 'express' // js diff
import { userCreateInput } from '../services/UserService' // js diff
const { AUTH_TOKEN_SALT } = process.env
const signUp = errorWrapper(async (req: Request, res: Response) => {
const { email, password }: userCreateInput = req.body
if (!email || !password) errorGenerator({ statusCode: 400 })
const foundUser = await UserService.findUser({ email })
if (foundUser) errorGenerator({ statusCode: 409 })
const hashedPassword = await bcrypt.hash(password, 10)
const createdUser = await UserService.createUser({
email,
password: hashedPassword
})
res.status(201).json({
message: 'user created',
email: createdUser.email
})
})
const logIn = errorWrapper(async (req: Request, res: Response) => {
const { email, password: inputPassword }: userCreateInput = req.body // js diff
if (!email || !inputPassword) errorGenerator({ statusCode: 400 })
const foundUser = await UserService.findUser({ email })
if (!foundUser) errorGenerator({ statusCode: 400 })
const { id, password: hashedPassword } = foundUser
const isValidPassword = await bcrypt.compare(inputPassword, hashedPassword)
if (!isValidPassword) errorGenerator({ statusCode: 400 })
const token = jwt.sign({ id }, AUTH_TOKEN_SALT)
res.status(200).json({ message: 'login success', token })
})
export default { // js diff
logIn,
signUp
}
//js와 달리 express Request, Response 임포트해 사용함
// js와 달리 userCreateInput이란 인스턴스를 서비스에 생성하고 해당 인스턴스에 맞춰 내용을 집어넣고?사용하는 듯 하다.
// Q userCreateInput 과 같은 인스턴스를 추가로 생성하여 이를 활용하는 이유
//
ArticleController.ts
import { ArticleService } from '../services'
import { errorWrapper, errorGenerator } from '../errors'
import { Request, Response } from 'express' // js diff
import { articleCreateInput } from '../services/ArticleService' // js diff
import { Article } from '../utils' // js diff
const getArticles = errorWrapper(async (req: Request, res: Response) => {
const articles = await ArticleService.findArticles(req.query)
res.status(200).json({ articles })
})
const getArticle = errorWrapper(async (req: Request, res: Response) => {
const { article_id } = req.params
const article = await ArticleService.findArticle({ id: Number(article_id) })
if (article.deleted_at) return res.status(200).json({ message: 'deleted' })
res.status(200).json({ article })
})
const postArticle = errorWrapper(async (req: Request, res: Response) => {
const { id: user_id } = req.foundUser
const { title, body }: articleCreateInput = req.body
if (!title || !body) errorGenerator({ statusCode: 400 })
const createdArticle = await ArticleService.createArticle({
user_id,
title,
body
})
res.status(201).json({ createdArticle })
})
const updateArticle = errorWrapper(async (req: Request, res: Response) => {
const { id: userIdFromToken } = req.foundUser
const { article_id } = req.params
const isValidFields = Article.validFields(Object.keys(req.body), ['title', 'body', 'status'])
if (!isValidFields) errorGenerator({ statusCode: 400 })
const foundArticle = await ArticleService.findArticle({ id: Number(article_id) })
const { user_id: userIdFromArticle } = foundArticle
if (userIdFromToken !== userIdFromArticle) errorGenerator({ statusCode: 403 })
const updatedArticle = await ArticleService.updateArticle({ id: Number(article_id), ...req.body })
res.status(201).json({ updatedArticle })
})
const publishArticle = errorWrapper(async (req: Request, res: Response) => {
const { id: userIdFromToken } = req.foundUser
const { article_id } = req.params
const foundArticle = await ArticleService.findArticle({ id: Number(article_id) })
const { user_id: userIdFromArticle } = foundArticle
if (userIdFromToken !== userIdFromArticle) errorGenerator({ statusCode: 403 })
const publishedArticle = await ArticleService.publishArticle(Number(article_id))
res.status(201).json({ publishedArticle })
})
const deleteArticle = errorWrapper(async (req: Request, res: Response) => {
const { id: userIdFromToken } = req.foundUser
const { article_id } = req.params
const foundArticle = await ArticleService.findArticle({ id: Number(article_id) })
const { user_id: userIdFromArticle } = foundArticle
if (userIdFromToken !== userIdFromArticle) errorGenerator({ statusCode: 403 })
const deletedArticle = await ArticleService.deleteArticle(Number(article_id))
res.status(201).json({ deletedArticle })
})
export default {
getArticles,
getArticle,
postArticle,
updateArticle,
publishArticle,
deleteArticle,
}
// 임포트 부분에서부터 차이점을 확인
// 전체적으로 js와 상당히 비슷함을 확인함
// req로 들어오는 요청을 서비스에서 특정 인스턴스로 만들어 활용하는 점이 다름 (User에서도 질문부분)
// js에 비해 ts 조금 더 세밀하게 지정해주는 부분들이 있음
// ex req, res ==> req:Request, res : Response
// ex const foundArticle = await ArticleService.findArticle({ id: articleId })
// == > const foundArticle = await ArticleService.findArticle({ id: Number(article_id) })
// Article.ts 라는 모듈화를 진행한 파일에 대한 이해가 필요! (Question)
Services
UserService.ts
import prisma from '../prisma'
export interface userCreateInput {
email: string,
password: string
}
export interface userUniqueSearchInput {
id?: number,
email?: string
}
const createUser = (data: userCreateInput) => {
return prisma.users.create({ data })
}
const findUser = (data: userUniqueSearchInput) => {
const [uniqueKey] = Object.keys(data)
return prisma.users.findUnique({ where: {[uniqueKey]: data[uniqueKey]}})
}
export default {
createUser,
findUser
}
ArticleService.ts
import prisma from '../prisma'
import { articles_status } from '@prisma/client'
import { Article } from '../utils'
const ARTICLES_DEFAULT_OFFSET = 0
const ARTICLES_DEFAULT_LIMIT = 5
export interface articleSearchInput {
offset?: number
limit?: number
user_id?: string
title?: string
body?: string
comments?: string
}
const findArticles = (query: articleSearchInput) => {
const { offset, limit, ...fields } = query
const where = Article.makeQueryOption(fields)
return prisma.articles.findMany({
where,
skip: Number(offset) || ARTICLES_DEFAULT_OFFSET,
take: Number(limit) || ARTICLES_DEFAULT_LIMIT,
orderBy: {
created_at: 'desc',
},
})
}
export interface articleUniqueSearchInput {
id: number
}
const findArticle = (data: articleUniqueSearchInput) => {
const [uniqueKey] = Object.keys(data)
return prisma.articles.findUnique({
where: { [uniqueKey]: data[uniqueKey] },
include: {
users: {
select: {
id: true,
email: true
}
},
comments: {
where: { deleted_at: null }
}
}
})
}
export interface articleCreateInput {
user_id: number,
title: string,
body: string,
}
const createArticle = (data: articleCreateInput) => {
return prisma.articles.create({ data })
}
export interface articleUpdateInput {
id: number,
title?: string,
body?: string,
status?: articles_status
}
const updateArticle = (data: articleUpdateInput) => {
const { id } = data
return prisma.articles.update({
where: { id },
data: {
...data,
updated_at: new Date()
}
})
}
const publishArticle = (id: number) => {
return prisma.articles.update({
where: { id },
data: { status: 'PUBLISHED', updated_at: new Date() }
})
}
const deleteArticle = (id: number) => {
return prisma.articles.update({
where: { id },
data: { status: 'DELETED', deleted_at: new Date() }
})
}
export default {
findArticles,
findArticle,
createArticle,
updateArticle,
publishArticle,
deleteArticle
}
// 전반적으로 Article.ts에 대한 이해가 필요
// 요청으로 들어온 쿼리를 매핑? 해주는 역할을 하는 것으로 예상..
errors
errorGenerator.ts
const DEFAULT_HTTP_STATUS_MESSAGES = {
400: 'Bad Requests',
401: 'Unauthorized',
403: 'Foribdden',
404: 'Not Found',
409: 'duplicate',
500: 'Internal Server Error',
503: 'Temporary Unavailable',
}
export interface ErrorWithStatusCode extends Error {
statusCode?: number
}
const errorGenerator = ({ message = '', statusCode = 500 }: { message?: string, statusCode: number}): void => {
const err: ErrorWithStatusCode = new Error(message || DEFAULT_HTTP_STATUS_MESSAGES[statusCode])
err.statusCode = statusCode
throw err
}
export default errorGenerator
errorWrapper.ts
import { RequestHandler, Request, Response, NextFunction } from 'express'
const errorWrapper = (controller: RequestHandler) => async (req: Request, res: Response, next: NextFunction) => {
try {
await controller(req, res, next)
} catch (err) {
next(err)
}
}
export default errorWrapper
middlewares
validationToken.ts
import jwt from 'jsonwebtoken'
import { errorGenerator, errorWrapper } from '../errors'
import { UserService } from '../services'
import { Express, Request, Response, NextFunction } from 'express'
import { users } from '@prisma/client'
const { AUTH_TOKEN_SALT } = process.env
export interface Token {
id: number
}
declare global {
module Express {
export interface Request {
foundUser: users
}
}
}
const validateToken = errorWrapper(async (req: Request, res: Response, next: NextFunction) => {
const [bearer, token] = req.headers.authorization.split(' ')
const { id } = jwt.verify(token, AUTH_TOKEN_SALT) as Token
const foundUser = await UserService.findUser({ id })
if (!foundUser) errorGenerator({ statusCode: 400 })
req.foundUser = foundUser
next()
})
export default validateToken
utills
Article.ts
import { articleSearchInput } from '../services/ArticleService'
import { articles_status } from '@prisma/client'
const DEFAULT_QUERY_OPTION: { deleted_at: null, status: articles_status } = {
deleted_at: null,
status: 'PUBLISHED'
}
const getQueryOption = <T>(key: string, value: T): object => {
const mapper = {
user_id: { [key]: Number(value) },
comments: { [key]: { some: { body: { contains: value } } } },
}
const matched = mapper[key]
if (matched) return matched
return { [key]: { contains: value } }
}
const makeQueryOption = (fields: articleSearchInput) => {
if (!fields) return DEFAULT_QUERY_OPTION
const defaultQueryOptions = Object.entries(DEFAULT_QUERY_OPTION).map(([key, value]) => ({ [key]: value }))
const queryOptins = Object.entries(fields).map(([key, value]) => getQueryOption(key, value))
const where = { AND: [...defaultQueryOptions, ...queryOptins] }
return where
}
const validFields = (requested: string[], allowed: string[]) => requested.every((field) => allowed.includes(field))
export default {
makeQueryOption,
validFields
}
// js에는 없으나 Ts에는 있는 내용으로 요청으로 들어온 쿼리를 매핑? 해주는 함수들로 예상
// Q : Atricle.ts의 역할
app.ts
import express, { Express, ErrorRequestHandler, Request, Response, NextFunction} from 'express'
import { ErrorWithStatusCode } from './errors/errorGenerator'
import morgan from 'morgan'
import routes from './routes'
const logger = morgan('dev')
const app: Express = express()
app.use(express.json())
app.use(logger)
app.use(routes)
const generalErrorHandler: ErrorRequestHandler = (err: ErrorWithStatusCode, req: Request, res: Response, next: NextFunction) => {
const { message, statusCode } = err
console.error(err)
res.status(statusCode || 500).json({ message })
}
app.use(generalErrorHandler)
export default app
server.ts
import dotenv from 'dotenv'
import http, { Server } from 'http'
import app from './app'
import prisma from './prisma'
dotenv.config()
const { PORT } = process.env
const server: Server = http.createServer(app)
const start = async () => {
try {
server.listen(PORT, () => console.log(`Server is listening on ${PORT} 🚀`))
} catch (err) {
console.error(err)
} finally {
await prisma.$disconnect()
}
}
start()