๐ป ์ฝ๋
https://github.com/jokj624/authCRUD-TS
๐ค why?
๊ณง .. ๋ค๊ฐ์ฌ ์ฑ์ผ์ ์๋๊ณ ์๋ฒ ๋งํ๋ ๊ฐ์์ธ ๋ด๊ฐ ์กฐ๊ธ์ด๋ผ๋ ๊ณต๋ถ๋ฅผ ํด์ผ๊ฒ ๋ค ํ๊ณ ๋ก๊ทธ์ธ/ํ์๊ฐ์ ์ 3 Layer Architecture ๋ก ์ค๊ณํด๋ณด์ ํ๊ณ ๊ณต๋ถ๋ฅผ ์์ํ๋ค.
์๋๋ api ๋ด์ ๋ผ์ฐํธ, ์ปจํธ๋กค๋ฌ, ์๋น์ค ๋ก์ง์ ๋ค ๋ฃ์ด๋๋ ์์ผ๋ก ๊ตฌํํ๋๋ฐ ์ ๋๋ก ์ค๊ณํด๋ณด๊ณ ์ถ๋ค.
๋ง์ ๋ธ๋ก๊ทธ์ SOPT ์ธ๋ฏธ๋์์ ํ ๋ด์ฉ, github๋ค์ ์ฐธ๊ณ ํด์ ํด๋ณด์๋ค.
์์ง ์ฝ๋๋ฅผ ์กฐ๊ธ ๋ ๋ฆฌํฉํ ๋ง ํด์ผํ์ง๋ง ๊ฐ๋จํ ๊ตฌํ ๋ด์ฉ๋ค์ ์ ๋ฆฌํ ๊ฒธ, ๊ธ์ ์์ฑํด๋ณธ๋ค.
โ๏ธ ํ๋ก์ ํธ ๊ตฌ์กฐ์ DB ์ค๊ณ
ํ๋ก์ ํธ ๊ตฌ์กฐ
src _____________ Loaders _______ db.ts
nodemon.json |_ api ___________________ middleware _ auth.ts
package.json | |_ route ______ index.ts
tsconfig.json | |_ UserRouter.ts
|_ config __ index.ts
|_ models __ User.ts
|_ interfaces __ IUser.ts
|_ errors _________________ errorGenerator.ts
| |_ generalErrorHandler.ts
|_ controllers ____________ index.ts
| |_ UserContorller.ts
|_ service ________________ inedx.ts
| |_ UserService.ts
|_ index.ts
ํ๋ก์ ํธ ๊ตฌ์กฐ๋ 3๊ณ์ธต ์ค๊ณ์ ๋ง๊ฒ Route => Controller => Service ์์ ์ ๋ฐ๋ก ํ๋จ ๊ณ์ธต์๋ง ์์กดํ๋๋ก ์ค๊ณํ๋ค.
์ต๋ํ ๋ชจ๋ํ ์์ผ๋ณด์๋ค.
User DB
src/models/user.ts
import mongoose from "mongoose";
import { IUser } from "../interfaces/IUser";
const UserSchema = new mongoose.Schema({
name: {
type: String,
},
email: {
type: String,
unique: true,
},
password: {
type: String,
},
avatar: {
type: String,
},
date: {
type: Date,
default: Date.now,
},
});
export default mongoose.model<IUser & mongoose.Document>("User", UserSchema);
์ฐ์ต์ด๋ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์ด๋ฆ, ์ด๋ฉ์ผ, ํจ์ค์๋, ์๋ฐํ(๊ธฐ๋ณธ ํ๋กํ ์ฌ์ง), ๊ฐ์ ๋ ์ง๋ก ์ค๊ณํ๋ค.
Interface
src/interfaces/IUser.ts
export interface IUser{
name: string;
email: string;
password: string;
avatar: string;
date: Date;
}
export interface IUserInputDTO {
name: string;
email: string;
password: string;
avatar?: string;
date?: Date;
}
export interface userUniqueSearchInput {
email : string;
}
user ์ธํฐํ์ด์ค์์๋ ํ์ post ํ ๋ ์ฌ์ฉํ DTO๋ฅผ ๋ง๋ค์ด ์ฃผ์๋ค.
InputDTO๋ ํ์๊ฐ์ ์, Search๋ ์ค๋ณต ์ด๋ฉ์ผ์ ์ฐพ์ ๋ ์ฌ์ฉํ ์ธํฐํ์ด์ค
์ฌ๊ธฐ์๋ ์ด๋ฉ์ผ๋ง์ผ๋ก ์ฐพ์์ง๋ง
email?, name? ๊ฐ์ด ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํด์ name์ผ๋ก ์ฐพ์๋ ๋๋ค.
โ๏ธ Router
src/api/route/index.ts
import { Router } from 'express';
import UserRouter from './UserRouter';
const router = Router();
router.use('/users', UserRouter);
export default router;
UserRouter ๋ชจ๋์์ ์ง์ ์ ์ธ ๋ผ์ฐํ ์ ํ ๊ฒ ์ด๋ฏ๋ก UserRouter ์๋ํฌ์ธํธ๋ฅผ ์ ์ํด์ค๋ค.
src/api/route/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;
ํ์๊ฐ์ (signUp), ๋ก๊ทธ์ธ(logIn)์ ์ฌ์ฉํ ์๋ํฌ์ธํธ๋ฅผ ์ ์ํด์ค๋ค.
๋ชจ๋ ์ฒ๋ฆฌ๋ UserController ๋ชจ๋ ๋ด ํจ์์์ ์งํํ๋ค.
๊ถ๊ธํ์ ์ด ์๋๋ฐ ์ router.use ์์ use๋ฅผ post, get ๊ฐ์ HTTP Method๋ก ํ์ํด๋ ๋๋ ๊ฑด๊ฐ?
์ฐพ์๋ด์ผ ๊ฒ ๋ค.
โ๏ธ Controller
src/contorllers/index.ts
import UserController from './UserController';
// ๋ชจ๋์ ํจํค์งํ ํ์ฌ ๊ฐ์ฒด๋ก ๋ด๋ณด๋ธ๋ค.
export{
UserController //(= UserController : UserController)
}
UserContorller ๋ฅผ ํจํค์งํ ํ์ฌ export ํ๋ค.
src/controllers/UserContorller.ts
import express, { NextFunction, Request, Response } from "express";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import config from "../config";
import errorGenerator from "../errors/errorGenerator";
import gravatar from 'gravatar';
import { check, validationResult } from "express-validator";
import { IUserInputDTO } from "../interfaces/IUser";
import { UserService } from "../services";
import { nextTick } from "process";
const signUp = async (req: Request, res: Response, next: NextFunction) => {
check("name", "Name is required").not().isEmpty();
check("email", "Please include a valid email").isEmail();
check("password", "Please enter a password with 6 or more characters").isLength({ min: 6 });
const { name, email, password } : IUserInputDTO = req.body;
try{
const errors = validationResult(req.body);
if(!errors.isEmpty()){
return res.status(400).json({ errors: errors.array() });
}
const foundUser = await UserService.findEmail({ email });
if(foundUser) errorGenerator({ statusCode: 409 }); // ์ด๋ฏธ ๊ฐ์
ํ ์ ์
const avatar = gravatar.url(email, {
s: "200",
r: "pq",
d: "mm",
});
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
const createdUser = await UserService.createUser({ name, email, password: hashedPassword, avatar: avatar });
const payload = {
user: {
email: createdUser.email,
},
};
jwt.sign(
payload,
config.jwtSecret,
{ expiresIn: 36000 },
(err, token) => {
if(err) throw err;
res.json({ token });
}
);
} catch (err) {
next(err);
}
};
export default {
signUp,
logIn
}
์ด๋ฒ ํฌ์คํ ์์๋ singUp ๋ง ๋ค๋ฃฐ ๊ฑฐ๋ผ logIn ํจ์๋ ๋บ๋ค.
DB์ ์ง์ ์ ๊ทผํ๋ ์ผ์ ๋ชจ๋ Service์์ ๊ตฌํ ํ ๊ฒ์ด๋ค.
check("name", "Name is required").not().isEmpty();
check("email", "Please include a valid email").isEmail();
check("password", "Please enter a password with 6 or more characters").isLength({ min: 6 });
const { name, email, password } : IUserInputDTO = req.body;
try{
const errors = validationResult(req.body);
if(!errors.isEmpty()){
return res.status(400).json({ errors: errors.array() });
}
const foundUser = await UserService.findEmail({ email });
if(foundUser) errorGenerator({ statusCode: 409 }); // ์ด๋ฏธ ๊ฐ์
ํ ์ ์
check๋ express-validator ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์กด์ฌํ๋ ํจ์๋ก, ๋ค์ํ ๊ฒ์ฆ์ ๋์์ค๋ค.
์ฌ๊ธฐ์๋ Name ํ๋๊ฐ ๋น์๋์ง, ์ด๋ฉ์ผ์ด ์ ๋๋ก ์กด์ฌํ๋์ง, ํจ์ค์๋ ๊ธธ์ด๊ฐ ์ต์ 6 ์ด์์ธ์ง ๊ฒ์ฌํ๊ณ ๋์ด๊ฐ๋ค.
์ดํ req.body์์ ์ธํฐํ์ด์ค์์ ์ ์ํ DTO ํ์ ์ผ๋ก body ๋ด์ฉ์ ๊บผ๋ด์ค๋ค.
validationResult์์ ๋ง์ฐฌ๊ฐ์ง๋ก body๋ด์ฉ์ ๊ฒ์ฌํ๊ณ ์๋ฌ ๋ฐ์ ์ ์๋ฌ๋ฅผ ๋ฆฌํดํ๋ค.
์ดํ ์ค๋ณต๋ ์ ์ ์ธ์ง ๊ฒ์ฌ๋ฅผ ์ํด Service ๋ก์ง์ ๊ตฌํ๋์ด ์๋ findEmail ํจ์์ email์ ๋ณด๋ด์ค๋ค.
๋ง์ฝ ํด๋น ์ ์ ๊ฐ ์กด์ฌ ํ ์ ์๋ฌ ํธ๋ค๋ง ํจ์์ 409 (duplicate) ์ฝ๋๋ฅผ ๋ณด๋ด์ค๋ค.
const avatar = gravatar.url(email, {
s: "200",
r: "pq",
d: "mm",
});
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
const createdUser = await UserService.createUser({ name, email, password: hashedPassword, avatar: avatar });
avatar๋ gravatar ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํด์ ๊ธฐ๋ณธ ์ด๋ฏธ์ง ์ฃผ์๋ฅผ ๋ง๋ค์ด์ค๋ค.
์ดํ bcrypt๋ฅผ ํตํด password๋ฅผ ์์ ํ๊ฒ ์ํธํ ํ ๊ฒ์ด๋ค.
์ด์ Service๋ก์ง์ ์กด์ฌํ๋ createUserํจ์๋ก ์ด ์ ๋ณด๋ค์ ๋ณด๋ด DB์ ์ ์ฅํด์ฃผ๋ฉด ๋๋ค.
jwt.sign(
payload,
config.jwtSecret,
{ expiresIn: 36000 },
(err, token) => {
if(err) throw err;
res.json({ token });
}
);
config์ ๋ฏธ๋ฆฌ ์ ํด๋ jwt ์ ๋ณด๋ฅผ ์ด์ฉํด ํ ํฐ์ ๋ฐ๊ธํ๋ค.
์ด ํ ํฐ์ ํด๋ผ์ด์ธํธํํ ๋์ ธ์ฃผ๋ฉด ๋ค์ฉ๋๋ก ์ฌ์ฉํ ์ ์๊ฒ ๋๋ค.
โ๏ธ Service
src/services/index.ts
import UserService from './UserService';
export{
UserService
} // ๊ฐ์ฒด ๋ด๋ณด๋ด๊ธฐ
์ปจํธ๋กค๋ฌ์ ๋ง์ฐฌ๊ฐ์ง๋ก UserService ๋ชจ๋์ ๋ด๋ณด๋ธ๋ค.
src/services/UserService.ts
import { IUserInputDTO, userUniqueSearchInput } from "../interfaces/IUser";
import User from "../models/User";
const createUser = (data: IUserInputDTO) => {
const user = new User(data);
return user.save();
}
const findEmail = (data: userUniqueSearchInput) => {
const { email } = data;
return User.findOne({ email });
}
export default {
createUser,
findEmail
};
์๊น ํ์๊ฐ์ ์ปจํธ๋กค๋ฌ ํจ์์์ ๋ดค๋ ์๋น์ค ๋ก์ง ํจ์๋ค์ด๋ค.
createUser์์๋ data๋ฅผ ๋ฐ์ ์๋ก์ด User๋ฅผ ๋ง๋ค์ด DB์ save ํด์ค๋ค.
๋ง์ผ IUserInputDTO ์ธํฐํ์ด์ค ํ์ ๊ณผ ๋ง์ง ์๋ data๊ฐ ์จ๋ค๋ฉด ์๋ฌ๊ฐ ๋ ๊ฒ์ด๋ค.
findEmail ํจ์์์๋ email์ ๋ฐ์ findOne ํจ์๋ฅผ ์ด์ฉํด ํด๋น ์ ์ ๊ฐ ์กด์ฌํ๋์ง ๊ฒ์ฌํ๋ค.
์กด์ฌํ๋ค๋ฉด ํด๋น ์ ์ ๊ฐ์ฒด๋ฅผ return ํ ๊ฒ์ด๋ค.
โ๏ธ Error ํธ๋ค๋ง
src/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',
};
//interface ์ด์ฉํด Error ๊ฐ์ฒด์ statusCode key ์ถ๊ฐ
export interface ErrorWithStatusCode extends Error {
statusCode? : number
};
const errorGenerator = ({ msg='', statusCode=500}: { msg?: string, statusCode: number }): void => {
//์ธ์๋ก ๋ค์ด์ค๋ ๋ฉ์ธ์ง์ ์ํ ์ฝ๋๋ฅผ ๋งคํ
const err: ErrorWithStatusCode = new Error(msg || DEFAULT_HTTP_STATUS_MESSAGES[statusCode]);
err.statusCode = statusCode;
throw err;
}
export default errorGenerator;
์๋ฌ ํธ๋ค๋ฌ๋ ๋ฑํ ์์ด๋ ๋๋ค. ๊ทธ๋ฅ ์ปจํธ๋กค๋ฌ์์ ์ง์ res.json์ผ๋ก send ํด์ค๋ ๋๋ค.
ํ์ง๋ง ์ฐธ๊ณ ๋ธ๋ก๊ทธ์์ ํธ๋ค๋ง ํจ์๋ฅผ ๋ง๋ค์ด ๋์ผ๋ฉด ๊ฐํธํ๊ฒ ์ฌ์ฉํ ์ ์๋คํ์ฌ ๋๋ ๋ฐ๋ผ์ ๋ง๋ค์ด ๋ณด์๋ค.
๋ค์ํ ์ํ์ฝ๋๋ฅผ ๋ฏธ๋ฆฌ ์ ์ํด๋๊ณ ์ด์ ๋ง๋ ๋ฉ์์ง๋ฅผ error ๊ฐ์ฒด๋ก ์์ฑํ์ฌ ๋์ ธ์ค๋ค.
src/errors/generalErrorHandler.ts
import { ErrorRequestHandler, Request, Response, NextFunction } from 'express';
import { ErrorWithStatusCode } from './errorGenerator';
const generalErrorHandler: ErrorRequestHandler = (err: ErrorWithStatusCode, req: Request, res: Response, next: NextFunction) => {
const { message, statusCode } = err;
//console.error(err);
res.status(statusCode || 500).json({ message });
}
export default generalErrorHandler;
errorGenerator ์์ ์ ์ํ ์ธํฐํ์ด์ค ErrorWithStatusCode ์๋ฌ๋ฅผ ์ง์ response๋ก ๋ณด๋ด์ฃผ๊ฒ ๋๋ค.
์๋ฌ ํธ๋ค๋ง ํจ์๋ฅผ ์ฌ์ฉํ ๋๋ ๊ฐํธํ๊ฒ
if(foundUser) errorGenerator({ statusCode: 409 }); // ์ด๋ฏธ ๊ฐ์
ํ ์ ์
์ด๋ฐ์์ผ๋ก ์ํ ์ฝ๋๋ง ๋ณด๋ด์ฃผ๋ฉด ์์์ send ํด์ค๋ค.
โ๏ธ ๊ทธ ์ธ ์ฝ๋๋ค
src/index.ts
๊ฐ์ฅ ์ฒ์ ์คํ ๋ ์ฝ๋์ด๋ค.
import express, { Express } from "express";
import routes from './api/route';
import generalErrorHandler from "./errors/generalErrorHandler";
const app : Express = express();
import connectDB from "./Loaders/db";
// Connect Database
connectDB();
app.use(express.urlencoded());
app.use(express.json());
app.use(routes);
app.use(generalErrorHandler);
// error handler
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get("env") === "production" ? err : {};
// render the error page
res.status(err.status || 500);
res.render("error");
});
app
.listen(5000, () => {
console.log(`
################################################
๐ก๏ธ Server listening on port: 5000 ๐ก๏ธ
################################################
`);
})
.on("error", (err) => {
console.error(err);
process.exit(1);
});
Loaders์ ๋ง๋ค์ด๋ db์ฐ๊ฒฐ ํจ์์ ์๋ฒ ์คํ์ ์งํํ๋ค.
๐ ์ฐธ๊ณ ์๋ฃ