Typescript로 Node 프로젝트 만들기

유석현(SeokHyun Yu)·2022년 12월 16일
0

Node.js

목록 보기
27/29
post-thumbnail

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;

Tweet.ts

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

tweet.router.ts

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;

tweet.controller.ts

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("트윗이 삭제되었습니다.");
};

tweet.repository.ts

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,
    },
  });
};
profile
Backend Engineer

0개의 댓글