-
TIL. 100 Node.js Project_javascriptTIL/Node.js 2021. 1. 16. 23:07728x90반응형
출처: 위코드 춤추는 개발자
> 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()
728x90반응형'TIL > Node.js' 카테고리의 다른 글
TIL.99 Node.js_Project Architecture (0) 2021.01.15 TIL.98 Node.js란? (0) 2021.01.14