1. 환경설정
.env
PORT=8000
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX=100
DATABASE=twitter
DATABASE_USER=root
DATABASE_PASSWORD=0000
DATABASE_HOST=127.0.0.1
JWT_SECRET_KEY=secret
JWT_EXPIRES_IN=1d
BCRYPT_SALT=12
.gitignore
/dist
/node_modules
/dist
.env
package.json
{
"name": "typescript",
"version": "1.0.0",
"description": "Learning Typescript",
"main": "app.js",
"scripts": {
"build": "rm -rf dist && tsc",
"start": "cross-env NODE_ENV=production PORT=8080 pm2 start dist/app.js",
"dev": "tsc-watch --onSuccess \"node dist/app.js\"",
"tsconfig": "tsc --init"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fkstndnjs/TypeScript.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/fkstndnjs/TypeScript/issues"
},
"homepage": "https://github.com/fkstndnjs/TypeScript#readme",
"dependencies": {
"@types/cors": "^2.8.13",
"@types/express": "^4.17.15",
"@types/jsonwebtoken": "^8.5.9",
"@types/morgan": "^1.9.3",
"bcrypt": "^5.1.0",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"express-validator": "^6.14.2",
"helmet": "^6.0.1",
"jsonwebtoken": "^8.5.1",
"morgan": "^1.10.0",
"mysql2": "^2.3.3",
"pm2": "^5.2.2",
"sequelize": "^6.27.0",
"tsc-watch": "^6.0.0",
"typescript": "^4.9.4"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
config.ts
import dotenv from "dotenv";
dotenv.config();
const config = {
port: parseInt(process.env.PORT!),
rateLimit: {
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS!),
max: parseInt(process.env.RATE_LIMIT_MAX!),
},
db: {
database: process.env.DATABASE!,
user: process.env.DATABASE_USER!,
password: process.env.DATABASE_PASSWORD!,
host: process.env.DATABASE_HOST!,
},
jwt: {
secretKey: process.env.JWT_SECRET_KEY!,
expiresIn: process.env.JWT_EXPIRES_IN!,
},
bcrypt: {
saltRounds: parseInt(process.env.BCRYPT_SALT!),
},
};
export default config;
database.ts
import { Sequelize } from "sequelize";
import config from "./config";
const db = new Sequelize(
config.db.database,
config.db.user,
config.db.password,
{
host: config.db.host,
dialect: "mysql",
}
);
export default db;
2. 미들웨어
rateLimiter.ts
import { NextFunction, Request, Response } from "express";
import limiter from "express-rate-limit";
import config from "../config";
const rateLimiter = limiter({
windowMs: config.rateLimit.windowMs,
max: config.rateLimit.max,
message: "잠시 후에 시도해주세요.",
handler: (req: Request, res: Response, next: NextFunction, options) => {
res.status(options.statusCode).send(options.message);
},
});
export default rateLimiter;
auth.ts
import { NextFunction, Request, Response } from "express";
import jwt from "jsonwebtoken";
import config from "../config";
const auth = (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.get("Authorization");
if (authHeader && authHeader.startsWith("Bearer ")) {
const token = authHeader.split(" ")[1];
jwt.verify(token, config.jwt.secretKey, (err: any, data: any) => {
if (err) {
return res.status(401).send("토큰이 유효하지 않습니다.");
}
if (!data) {
return res.status(401).send("토큰이 유효하지 않습니다.");
} else {
req.headers["userId"] = data.id;
return next();
}
});
} else {
return res.status(401).send("토큰이 유효하지 않습니다.");
}
};
export default auth;
validate.ts
import { NextFunction, Request, Response } from "express";
import { validationResult } from "express-validator";
export default function validate(
req: Request,
res: Response,
next: NextFunction
) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).send(errors.array().map((error) => error.msg));
}
next();
}
3. 엔티티
User.ts
import { DataTypes } from "sequelize";
import db from "../database";
const User = db.define(
"user",
{
id: {
type: DataTypes.BIGINT,
autoIncrement: true,
allowNull: false,
primaryKey: true,
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
},
password: {
type: DataTypes.STRING(100),
allowNull: false,
},
username: {
type: DataTypes.STRING(100),
allowNull: false,
},
email: {
type: DataTypes.STRING(100),
allowNull: false,
},
},
{
timestamps: true,
}
);
export default User;
import { DataTypes } from "sequelize";
import db from "../database";
import User from "./user";
const Tweet = db.define("tweet", {
id: {
type: DataTypes.BIGINT,
autoIncrement: true,
allowNull: false,
primaryKey: true,
},
text: {
type: DataTypes.TEXT,
allowNull: false,
},
});
Tweet.belongsTo(User);
export default Tweet;
4. 서버 시작
app.ts
import express, { NextFunction, Request, Response } from "express";
import helmet from "helmet";
import morgan from "morgan";
import authRouter from "./auth/auth.router";
import config from "./config";
import db from "./database";
import rateLimiter from "./middleware/rateLimiter";
import tweetRouter from "./tweet/tweet.router";
const app = express();
app.use(express.json());
app.use(morgan("dev"));
app.use(helmet());
app.use(rateLimiter);
app.use("/auth", authRouter);
app.use("/tweet", tweetRouter);
app.use((req: Request, res: Response, next: NextFunction) => {
res.status(404).send("NOT FOUND");
});
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
res.status(500).send(err);
});
db.sync().then(() => {
app.listen(config.port, () => {
console.log("Server On...");
});
});
5. 유저 API
auth.router.ts
import express from "express";
import { body } from "express-validator";
import validate from "../middleware/validate";
import * as authController from "./auth.controller";
const authRouter = express.Router();
const loginValidation = [
body("username").notEmpty().withMessage("아이디를 입력해주세요."),
body("password").notEmpty().withMessage("비밀번호를 입력해주세요."),
validate,
];
const signupValidation = [
...loginValidation,
body("name").notEmpty().withMessage("이름을 입력해주세요."),
body("email")
.isEmail()
.normalizeEmail()
.withMessage("이메일 형식에 맞지 않습니다."),
validate,
];
authRouter.post("/signup", signupValidation, authController.signup);
authRouter.post("/login", loginValidation, authController.login);
export default authRouter;
auth.controller.ts
import { NextFunction, Request, Response } from "express";
import jwt from "jsonwebtoken";
import config from "../config";
import * as UserRepository from "../repository/user.repository";
import bcrypt from "bcrypt";
const createJWTToken = (id: number) => {
const token = jwt.sign({ id }, config.jwt.secretKey, {
expiresIn: config.jwt.expiresIn,
});
return token;
};
export const signup = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { name, username, password, email } = req.body;
const foundUser = await UserRepository.getByUsername(username);
if (foundUser) {
return res.status(409).send("이미 존재하는 사용자입니다.");
}
const hashedPassword = await bcrypt.hash(password, config.bcrypt.saltRounds);
const user = await UserRepository.createUser({
name,
username,
password: hashedPassword,
email,
});
const token = createJWTToken(user.id);
res.status(201).json({ message: `회원가입이 완료되었습니다!`, token });
};
export const login = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { username, password } = req.body;
const foundUser = await UserRepository.getByUsername(username);
if (!foundUser) {
return res.status(401).send("아이디 혹은 비밀번호가 틀렸습니다.");
}
const compareResult = await bcrypt.compare(password, foundUser.password);
if (!compareResult) {
return res.status(401).send("아이디 혹은 비밀번호가 틀렸습니다.");
}
const token = createJWTToken(foundUser.id);
res.status(201).json({ message: `환영합니다, ${username}님!`, token });
};
user.repository.ts
import User from "../entities/user";
export const getByUsername = async (username: string) => {
const user = User.findOne({
where: {
username,
},
}).then((data) => {
console.log(data);
return data?.dataValues;
});
return user;
};
export const createUser = async (user: {
name: string;
username: string;
password: string;
email: string;
}) => {
return User.create(user).then((data) => {
return data.dataValues;
});
};
6. 트윗 API
import express from "express";
import auth from "../middleware/auth";
import * as tweetController from "./tweet.controller";
const tweetRouter = express.Router();
tweetRouter.get("/", auth, tweetController.getAllTweets);
tweetRouter.get("/:tweetId", auth, tweetController.getTweetById);
tweetRouter.post("/", auth, tweetController.createTweet);
tweetRouter.put("/:tweetId", auth, tweetController.updateTweet);
tweetRouter.delete("/:tweetId", auth, tweetController.deleteTweet);
export default tweetRouter;
import { NextFunction, Request, Response } from "express";
import * as tweetRepository from "./tweet.repository";
export const getAllTweets = async (
req: Request,
res: Response,
next: NextFunction
) => {
const tweets = await tweetRepository.getAllTweets();
res.status(200).json(tweets);
};
export const getTweetById = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tweetId } = req.params;
const tweet = await tweetRepository.getTweetById(tweetId);
res.status(200).json(tweet);
};
export const createTweet = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { text } = req.body;
const userId = req.headers.userId as string;
const tweet = await tweetRepository.createTweet({ text, userId });
res.status(201).json(tweet);
};
export const updateTweet = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tweetId } = req.params;
const { text } = req.body;
await tweetRepository.updateTweet(tweetId, text);
res.status(200).send("트윗이 수정되었습니다.");
};
export const deleteTweet = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tweetId } = req.params;
await tweetRepository.deleteTweet(tweetId);
res.status(204).send("트윗이 삭제되었습니다.");
};
import Tweet from "../entities/tweet";
import User from "../entities/user";
export const getAllTweets = async () => {
return Tweet.findAll({
attributes: ["id", "text"],
include: {
model: User,
attributes: ["id", "username"],
},
order: [["createdAt", "DESC"]],
});
};
export const getTweetById = async (id: string) => {
return Tweet.findByPk(id, {
attributes: ["id", "text"],
include: {
model: User,
attributes: ["id", "username"],
},
}).then((data) => {
return data?.dataValues;
});
};
export const createTweet = async (tweet: { text: string; userId: string }) => {
return Tweet.create(tweet).then((data) => data.dataValues);
};
export const updateTweet = async (tweetId: string, text: string) => {
Tweet.update(
{
text,
},
{
where: {
id: tweetId,
},
}
);
};
export const deleteTweet = async (tweetId: string) => {
Tweet.destroy({
where: {
id: tweetId,
},
});
};