안녕하세요, 코린이의 코딩 학습기 채니 입니다.
개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.
RESTful API란?
https://spoqa.github.io/2012/02/27/rest-introduction.html
https://meetup.nhncloud.com/posts/92
https://sanghaklee.tistory.com/57
RESTful API 만들기
models/index.ts
import { Sequelize } from "sequelize";
import config from "../config/config";
import User from "./user";
import Post from "./post";
import Tag from "./tag";
import PostTag from "./postTag";
interface IDb {
sequelize: Sequelize;
}
// MySQL 연결 객체 생성
const { database, username, password } = config["development"];
let sequelize = new Sequelize(
database,
username,
password,
config["development"]
);
const db: IDb = { sequelize };
User.initiate(sequelize);
Post.initiate(sequelize);
Tag.initiate(sequelize);
PostTag.initiate(sequelize);
User.associate();
Post.associate();
Tag.associate();
export default db;
models/user.ts
import { Sequelize, Model, DataTypes } from "sequelize";
import Post from "./post";
class User extends Model {
public readonly id!: number;
public username!: string;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
static initiate(sequelize: Sequelize) {
User.init(
{
username: {
type: DataTypes.STRING(10),
allowNull: false,
},
},
{
sequelize,
timestamps: true,
underscored: true,
modelName: "User",
tableName: "users",
paranoid: false,
charset: "utf8",
collate: "utf8_general_ci",
}
);
}
static associate() {
User.hasMany(Post, { foreignKey: "user_id" });
}
}
export default User;
models/post.ts
import {
Sequelize,
Model,
DataTypes,
BelongsToManyAddAssociationMixin,
} from "sequelize";
import User from "./user";
import Tag from "./tag";
class Post extends Model {
public readonly id!: number;
public title!: string;
public content!: string;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
public tags!: Tag[];
public addTag!: BelongsToManyAddAssociationMixin<Tag, string>;
static initiate(sequelize: Sequelize) {
Post.init(
{
title: {
type: DataTypes.STRING(15),
allowNull: false,
},
content: {
type: DataTypes.STRING(200),
allowNull: false,
},
},
{
sequelize,
timestamps: true,
underscored: true,
modelName: "Post",
tableName: "posts",
paranoid: false,
charset: "utf8mb4",
collate: "utf8mb4_general_ci",
}
);
}
static associate() {
Post.belongsTo(User, { foreignKey: "user_id" });
Post.belongsToMany(Tag, { through: "postTag" });
}
}
export default Post;
models/tag.ts
import { Sequelize, Model, DataTypes } from "sequelize";
import db from ".";
import Post from "./post";
interface TagAttributes {
title: string;
}
class Tag extends Model<TagAttributes> {
public readonly id!: number;
public title!: string;
static initiate(sequelize: Sequelize) {
Tag.init(
{
title: {
type: DataTypes.STRING(10),
allowNull: false,
// unique: true,
},
},
{
sequelize,
timestamps: false,
underscored: true,
modelName: "tag",
tableName: "tags",
paranoid: false,
charset: "utf8mb4",
collate: "utf8mb4_general_ci",
}
);
}
static associate() {
Tag.belongsToMany(Post, { through: "postTag" });
}
}
export default Tag;
models/postTag.ts → 추후 브릿지 테이블에서 삭제를 위해 모델 생성
import { Sequelize, Model, DataTypes } from "sequelize";
class PostTag extends Model {
public post_id!: number;
public tag_id!: number;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
static initiate(sequelize: Sequelize) {
PostTag.init(
{
post_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
tag_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
},
{
sequelize,
timestamps: true,
underscored: true,
modelName: "PostTag",
tableName: "postTag",
paranoid: false,
charset: "utf8mb4",
collate: "utf8mb4_general_ci",
}
);
}
}
export default PostTag;
app.ts
import express from "express";
import db from "./models";
import dotenv from "dotenv";
import apiRouter from "./routes/apiRouter";
import bodyParser from "body-parser";
dotenv.config();
const app = express();
app.set("port", process.env.PORT);
db.sequelize
.sync({ force: false })
.then(() => {
console.log("데이터베이스 연결 성공");
})
.catch((err) => {
console.error(err);
});
app.use(bodyParser.json());
app.use("/api", apiRouter);
app.listen(app.get("port"), () => {
console.log(app.get("port"), "번에서 대기 중!");
});
routes/apiRouter.ts
import express from "express";
import userRouter from "./userRouter";
import postRouter from "./postRouter";
const app = express();
app.use("/users", userRouter);
app.use("/posts", postRouter);
export default app;
routes/userRoutes.ts
import express from "express";
import {
getUser,
getUsers,
createUser,
updateUser,
deleteUser,
} from "../controllers/user";
const router = express.Router();
router.get("/", getUsers);
router.get("/:id", getUser);
router.post("/", createUser);
router.put("/:id", updateUser);
router.delete("/:id", deleteUser);
export default router;
controllers/user.ts
import User from "../models/user";
import { NextFunction, Request, Response } from "express";
// GET /users
const getUsers = async (req: Request, res: Response, next: NextFunction) => {
try {
const users = await User.findAll();
return res.status(200).json(users);
} catch (error) {
console.error(error);
return res.status(500).json({ message: "서버 오류" });
}
};
// GET /users/:id
const getUser = async (req: Request, res: Response, next: NextFunction) => {
try {
const id = Number(req.params.id);
const user = await User.findOne({ where: { id } });
if (!user) {
return res.status(404).json({ message: "검색 결과가 없습니다." });
}
return res.status(200).json(user);
} catch (error) {
console.error(error);
return res.status(500).json({ message: "서버 오류" });
}
};
// POST /users
const createUser = async (req: Request, res: Response, next: NextFunction) => {
try {
const { username }: { username: string } = req.body;
const user = await User.create({ username });
return res.status(201).json(user);
} catch (error) {
console.log(error);
return res.status(500).json({ message: "서버 오류" });
}
};
// PUT /users/:id
const updateUser = async (req: Request, res: Response, next: NextFunction) => {
try {
const { username }: { username: string } = req.body;
const id = Number(req.params.id);
const [affectedCount] = await User.update(
{
username,
},
{
where: { id },
}
);
if (!affectedCount) {
return res.status(404).json({ message: "존재하지 않는 회원입니다." });
}
const user = await User.findOne({ where: { id } });
return res.status(200).json(user);
} catch (error) {
console.error(error);
return res.status(500).json({ message: "서버 오류" });
}
};
// DELETE /users/:id
const deleteUser = async (req: Request, res: Response, next: NextFunction) => {
try {
const id = Number(req.params.id);
await User.destroy({ where: { id } });
return res.status(200).json({ message: "삭제 완료" });
} catch (error) {
console.error(error);
return res.status(500).json({ message: "서버 오류" });
}
};
export { getUsers, getUser, createUser, updateUser, deleteUser };
routes/postRoutes.ts
import express from "express";
import {
getPosts,
getPost,
createPost,
updatePost,
deletePost,
} from "../controllers/post";
const router = express.Router();
router.get("/", getPosts);
router.get("/:id", getPost);
router.post("/", createPost);
router.put("/:id", updatePost);
router.delete("/:id", deletePost);
export default router;
controllers/post.ts
import { NextFunction, Request, Response } from "express";
import Post from "../models/post";
import Tag from "../models/tag";
import PostTag from "../models/postTag";
interface IPost {
title: string;
content: string;
user_id: number;
tags: string[];
}
// GET /posts
const getPosts = async (req: Request, res: Response, next: NextFunction) => {
try {
const posts = await Post.findAll({
include: [{ model: Tag, through: { attributes: [] } }], // 브릿지 테이블의 컬럼은 조회에서 제거
});
return res.status(200).json(posts);
} catch (error) {
console.error(error);
return res.status(500).json({ message: "서버 오류" });
}
};
// GET /posts/:id
const getPost = async (req: Request, res: Response, next: NextFunction) => {
try {
const id = Number(req.params.id);
const post = await Post.findOne({
where: { id },
include: [{ model: Tag }],
});
if (!post) {
return res.status(404).json({ message: "게시글이 존재하지 않습니다." });
}
return res.status(200).json(post);
} catch (error) {
console.error(error);
return res.status(500).json({ message: "서버 오류" });
}
};
// POST /posts
const createPost = async (req: Request, res: Response, next: NextFunction) => {
try {
const { title, content, user_id, tags }: IPost = req.body;
// 게시글 테이블에 저장
const post = await Post.create({
title,
content,
user_id,
});
// 태그 테이블에 저장
if (tags) {
tags.forEach(async (tag: string) => {
// 안쓰는 변수는 _로 함, [모델, 생성여부] 리턴
const [tagModel, _] = await Tag.findOrCreate({
where: { title: tag },
});
// 브릿지 테이블에 저장
await post.addTag(tagModel);
});
}
return res.status(201).json(post);
} catch (error) {
console.error(error);
return res.status(500).json({ message: "서버 오류" });
}
};
// PUT /posts/:id
const updatePost = async (req: Request, res: Response, next: NextFunction) => {
try {
let { title, content, tags }: IPost = req.body;
const id = Number(req.params.id);
// 게시글 수정
const [affectedCount] = await Post.update(
{ title, content },
{ where: { id } }
);
if (!affectedCount) {
return res.status(404).json({ message: "존재하지 않는 게시글입니다." });
}
const post = await Post.findOne({
where: { id },
include: [{ model: Tag, through: { attributes: [] } }],
});
// 태그 수정
if (tags) {
// 게시글에 딸린 기존 태그들 조회
const existedTags = await Tag.findAll({
attributes: ["id", "title"],
include: [{ model: Post, where: { id }, attributes: [] }],
});
existedTags.forEach((existedTag: Tag) => {
// 기존에 존재하는 태그 - 유지 / tags 배열에서 삭제
if (tags.includes(existedTag.title)) {
tags = tags.filter((tag: string) => tag !== existedTag.title);
} else {
// 기존에 존재하지 않는 태그 - 기존 태그를 브릿지 테이블에서 삭제
PostTag.destroy({ where: { tag_id: existedTag.id, post_id: id } });
}
});
console.log(tags);
// 태그 추가하기
if (tags.length) {
tags.forEach(async (tag: string) => {
const [tagModel, _] = await Tag.findOrCreate({
where: { title: tag },
});
// 브릿지 테이블에 저장
await post!.addTag(tagModel);
});
}
}
return res.status(200).json(post);
} catch (error) {
console.error(error);
return res.status(500).json({ message: "서버 오류" });
}
};
// DELETE /posts/:id
const deletePost = async (req: Request, res: Response, next: NextFunction) => {
try {
const id = Number(req.params.id);
await Post.destroy({ where: { id } });
return res.status(200).json({ message: "게시글 삭제 완료" });
} catch (error) {
console.error(error);
return res.status(500).json({ message: "서버 오류" });
}
};
export { getPosts, getPost, createPost, updatePost, deletePost };
회원 / 게시글 API 구현!
진행하면서 깨달은 지식 정리
- postman으로 요청을 보냈을 때 req.body가 undefined 였던 현상
body-parser 미들웨어(요청의 본문에 있는 데이터를 해석해 req.body 객체로 만들어주는 역할)이 없어서 그랬던 것
app.ts 파일에 body-parser 미들웨어 추가하여 오류 해결
import bodyParser from "body-parser";
...
app.use(bodyParser.json());
...
- include로 join하여 조회할 때, 브릿지 테이블의 컬럼들까지 다 같이 딸려나오는 현상
다 딸려나오는 코드
const posts = await Post.findAll({
include: [{ model: Tag }], // 브릿지 테이블의 컬럼은 조회에서 제거
});
여기서 attributes 속성을 적용해도 자꾸 딸려나와서.. 구글링해보니 아래와 같이 브릿지 테이블에 대한 attributes를 설정해야 했음!
const posts = await Post.findAll({
include: [{ model: Tag, through: { attributes: [] } }], // 브릿지 테이블의 컬럼은 조회에서 제거
});
- findOrCreate 혹은 update 등의 메소드 사용 시 리턴 값 받아 하드코딩..^^
하드코딩
// 게시글 수정
const success = await Post.update(
{ title, content },
{ where: { id } }
);
if (!success[0]) {
return res.status(404).json({ message: "존재하지 않는 게시글입니다." });
}
이처럼 update 시 리턴 값으로 Promise<[affectedCount:number]>를 주는데 이를 받아 [0]으로 하드코딩해서 가져오려고 했었음!
구조분해할당 이용
// 게시글 수정
const [affectedCount] = await Post.update(
{ title, content },
{ where: { id } }
);
if (!affectedCount) {
return res.status(404).json({ message: "존재하지 않는 게시글입니다." });
}
이처럼 받아서 사용하면 코드가 더 깔꼼쓰
이 때 만일, 리턴 값 중 사용하지 않는 값들은 '_'로 헷갈리지 않게 해주면 더 좋음
tags.forEach(async (tag: string) => {
const [tagModel, _] = await Tag.findOrCreate({
where: { title: tag },
});
findOrCreate는 Promise<[M, boolean]> 값을 리턴([모델, 생성여부])하는데, 이를 [tagModel, created]로 받아주어야 하지만
created는 사용하지 않으므로 '_'로 처리!
'JavaScript > Node.js' 카테고리의 다른 글
Node, Express, typescript boilerplate - 2 (0) | 2023.01.12 |
---|---|
Node, Express, typescript boilerplate (0) | 2023.01.10 |
Node) 시퀄라이즈 정리 (0) | 2023.01.03 |
Node) 익스프레스로 SNS 서비스 만들기 - multer 패키지로 이미지 업로드 구현하기 (0) | 2023.01.02 |
Node) 익스프레스로 SNS 서비스 만들기 - Passport 모듈로 로그인 구현하기 (0) | 2023.01.02 |