반응형

node와 express를 활용한 프로젝트를 시작하고

간단한 CRUD api를 작성해보는 포스팅을 적어보려 한다.

 

프로젝트를 시작할 때 참고하면 좋을 것 같다.

 

DB 생성과 초기 세팅은 이전 포스팅 참고.

 

MongoDB Cluster 생성하기 (링크🔗)

Express 초기 세팅, yarn 사용법 (링크🔗)

 

 

 


 

.env 만들기

루트 디렉토리에 '.env' 파일을 만든다 (* 반드시 .gitignore에 추가한다.)

 

 

// .env
PORT=8080
MONGODB_URI=mongodb+srv://id:password@clusterName.dgufc.mongodb.net/어쩌구저쩌구?retryWrites=true&w=majority

위와 같이 입력하는데, 'MONGOB_URI'는 MongoDB 홈페이지에서 가져온다

 

홈페이지에서 로그인하면 아래 화면이 바로 나온다.

 

 

 

 

기타 파일 생성 및 수정

1. 'config/index.ts' 생성

import dotenv from "dotenv";

// Set the NODE_ENV to 'development' by default
process.env.NODE_ENV = process.env.NODE_ENV || "development";

const envFound = dotenv.config();
if (envFound.error) {
  // This error should crash whole process

  throw new Error("⚠️  Couldn't find .env file  ⚠️");
}

export default {
  /**
   * Your favorite port
   */
  port: parseInt(process.env.PORT as string, 10) as number,

  /**
   * MongoDB URI
   */
  mongoURI: process.env.MONGODB_URI as string,
};

 

2. 'loaders/db.ts' 생성

import mongoose from "mongoose";
import config from "../config";

const connectDB = async () => {
    try {
        await mongoose.connect(config.mongoURI);

        mongoose.set('autoCreate', true);

        console.log("Mongoose Connected ...");
    } catch (err: any) {
        console.error(err.message);
        process.exit(1);
    }
};

export default connectDB;

 

3. 'src/index.ts' 수정

import express, { Request, Response, NextFunction } from "express";
import config from "./config";
const app = express();
import connectDB from "./loaders/db";
import routes from './routes';
require('dotenv').config();

connectDB(); // DB 연결하기

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use(routes);   //라우터 분리
// error handler

interface ErrorType {
  message: string;
  status: number;
}

// 모든 에러에 대한 핸들링
app.use(function (err: ErrorType, req: Request, res: Response, next: NextFunction) {

  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(config.port, () => {
    console.log(`
    ################################################
          🛡️  Server listening on port 🛡️
    ################################################
  `);
  })
  .on("error", (err) => {
    console.error(err);
    process.exit(1);
  });

 

 

4. modules 폴더 만들기

// modules/util.ts -> success, fail 메세지 가공
const util = {
    success: (status: number, message: string, data?: any) => {
        return {
            status,
            success: true,
            message,
            data,
        };
    },
    fail: (status: number, message: string, data?: any) => {
        return {
            status,
            success: false,
            message,
        };
    },
};

export default util;

// modules/responseMessage.ts -> response message 가공
const message = {
    NULL_VALUE: '필요한 값이 없습니다.',
    NOT_FOUND: '존재하지 않는 자원',
    BAD_REQUEST: '잘못된 요청',
    INTERNAL_SERVER_ERROR: '서버 내부 오류',

    // 포스팅 조회
    READ_POST_SUCCESS: '포스팅 조회 성공',
    CREATE_POST_SUCCESS: '포스팅 생성 성공',
    DELETE_POST_SUCCESS: '포스팅 삭제 성공',
    UPDATE_POST_SUCCESS: '포스팅 수정 성공',
}

export default message;

// modules/statusCode.ts
const statusCode = {
    OK: 200,
    CREATED: 201,
    NO_CONTENT: 204,
    BAD_REQUEST: 400,
    UNAUTHORIZED: 401,
    FORBIDDEN: 403,
    NOT_FOUND: 404,
    CONFLICT: 409,
    INTERNAL_SERVER_ERROR: 500,
    SERVICE_UNAVAILABLE: 503,
    DB_ERROR: 600,
};

export default statusCode;

 

 

라우팅 설정하기

routes/PostRouter.ts

import { Router } from "express";
import { PostController } from "../controllers";

const router: Router = Router();

router.post('/', PostController.createPost);
router.put('/:postId', PostController.updatePost);
router.get('/:postId', PostController.findPostById);
router.delete('/:postId', PostController.deletePost);

export default router;
위에서 작성한대로 나머지를 작성하려고 한다.
PostController.ts를 만들어 createPost, updatePost, findPostById, deletePost 4개의 함수를 만든다.
put, get, delete 메서드에 대해서는 postId를 파라미터로 받는다.

 

routes/index.ts

//router index file
import { Router } from 'express';
import PostRouter from "./PostRouter";

const router: Router = Router();

router.use('/post', PostRouter);

export default router;

 

 

DTO 작성

DTO는 Data Type Object의 약자로,

데이터를 받아오거나 반환하는 규격으로 interface로 작성한다.

(장고의 serializer 생각하면 편하더라는..)

 

// interfaces/post/PostCreateDto.ts -> create 용
export interface PostCreateDto {
    title: string;
    content: string;
    additional?: {
        category: string;
        season: string;
    };
}

// interfaces/post/PostInfo.ts -> model 정의 용도
export interface PostInfo {
    title: string;
    content: string;
    additional: {
        category: string;
        season: string;
    };
}

// interfaces/post/PostResponseDto.ts -> 정보 조회 용
import mongoose from "mongoose";
import { PostCreateDto } from "./PostCreateDto";

export interface PostResponseDto extends PostCreateDto { // PostCreateDto 를 상속받아 확장시킨다.
    _id: mongoose.Schema.Types.ObjectId;
}


// interfaces/post/PostUpdateDto.ts -> update 용
import mongoose from "mongoose";

export interface PostUpdateDto {
    _id: mongoose.Schema.Types.ObjectId;
    title?: string;
    content?: string;
    additional?: {
        category: string;
        season: string;
    };
}

// interfaces/common/PostBaseResponseDto.ts -> id만 노출할 때
import mongoose from "mongoose";

export interface PostBaseResponseDto {
    _id: mongoose.Schema.Types.ObjectId;
}

다른 로직 작성하다가 필요에 맞게 DTO를 작성해주는 것이 좋다.

 

 

model 작성

models/Post.ts 를 만든다.

import mongoose from "mongoose";
import { PostInfo } from "../interfaces/post/PostInfo";

const PostSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true
    },
    content: {
        type: String,
        required: true,
    },
    dateTimeOfPosting: {
        type: Date,
        required: true,
        default: Date.now,
    },
    additional: {
        category: { type: String },
        season: { type: String },
    },
});

export default mongoose.model<PostInfo & mongoose.Document>("Post", PostSchema);

dateTimeOfPosting은 생성 당시 일시를 저장한다.

additional은 중복 document(MongoDB에서 row에 해당하는 개념)



 

Controller 작성

import express, { Request, Response } from "express";
import { PostCreateDto } from "../interfaces/post/PostCreateDto";
import { PostUpdateDto } from "../interfaces/post/PostUpdateDto";
import statusCode from "../modules/statusCode";
import message from "../modules/responseMessage";
import util from "../modules/util";
import { PostService } from "../services";


const createPost = async (req: Request, res: Response): Promise<void> => {
    const postCreateDto: PostCreateDto = req.body;
    
    try {
        const data = await PostService.createPost(postCreateDto);
        res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, message.CREATE_POST_SUCCESS, data));
    } catch (error) {
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

const updatePost = async (req: Request, res: Response): Promise<void> => {
    const postUpdateDto: PostUpdateDto = req.body;
    const { postId } = req.params;

    try {
        const data = await PostService.updatePost(postId, postUpdateDto);
        res.status(statusCode.CREATED).send(util.success(statusCode.OK, message.UPDATE_POST_SUCCESS, data));
    } catch (error) {
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

const findPostById = async (req: Request, res: Response): Promise<void> => {
    const { postId } = req.params;

    try {
        const data = await PostService.findPostById(postId);
        res.status(statusCode.CREATED).send(util.success(statusCode.OK, message.READ_POST_SUCCESS, data));
    } catch (error) {
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

const deletePost = async (req: Request, res: Response): Promise<void> => {
    const { postId } = req.params;

    try {
        const data = await PostService.deletePost(postId);
        res.status(statusCode.CREATED).send(util.success(statusCode.OK, message.DELETE_POST_SUCCESS, data));
    } catch (error) {
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

export default {
    createPost,
    updatePost,
    findPostById,
    deletePost,
}

Request body는 req.body에, Request parameters는 req.params에 저장되어 있어서

필요한 경우에 맞게 할당시켜 사용한다.

 

controllers/index.ts도 수정한다.

import PostController from "./PostController"

// controller index file
export {
   PostController,
}

 

 

Service 작성

import { PostBaseResponseDto } from "../interfaces/common/PostBaseResponseDto";
import { PostCreateDto } from "../interfaces/post/PostCreateDto";
import { PostResponseDto } from "../interfaces/post/PostResponseDtd";
import { PostUpdateDto } from "../interfaces/post/PostUpdateDto";
import Post from "../models/Post";



const createPost = async (postCreateDto: PostCreateDto): Promise<PostBaseResponseDto> => {
    try {
    	// create를 위해 각 filed명에 값들을 할당시켜준다.
        const post = new Post({
            title: postCreateDto.title,
            content: postCreateDto.content,
            additional: {
                category: postCreateDto.additional?.category,
                season: postCreateDto.additional?.season,
            }
        });
        await post.save();

        const data = {
            _id: post.id
        };

        return data;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

const updatePost = async (postId: string, postUpdateDto: PostUpdateDto): Promise<PostUpdateDto | null> => {
    try {
        await Post.findByIdAndUpdate(postId, postUpdateDto); // update 로직
        const post = await findPostById(postId); // update 된 정보를 불러오는 로직
        // null이 될 경우를 처리해줘야 한다.
		if (!post) {
            return null;
        }
        return post;

    } catch (error) {
        console.log(error);
        throw error;
    }
}

const findPostById = async (postId: string): Promise<PostResponseDto | null> => {
    try {
        const post = await Post.findById(postId);
        if (!post) {
            return null;
        }
        return post;
        
    } catch (error) {
        console.log(error);
        throw error;
    }
}

const deletePost = async (postId: string): Promise<PostResponseDto | null> => {
    try {
        const post = await Post.findByIdAndDelete(postId);
        if (!post) {
            return null;
        }
        return post;
    } catch (error) {
        console.log(error)
        throw error;
    }
}

export default {
    createPost,
    updatePost,
    findPostById,
    deletePost,
}

findById와 같은 메서드들은 mongoose 내장 함수이다.

용법이 그렇게 어렵지 않다.

 

 services/index.ts 도 수정해준다.

import PostService from "./PostService"

//service index file
export {
    PostService,
}

 

이렇게 작성이 끝이 났다.

 

 

요청 보내기

yarn run dev 실행

 

POST

PUT

userId는 무시해도 좋다(포스팅 내용과 다른 코드 때문)

 

GET

 

DELETE

 

 


 

프로젝트 구조에 대해 간단히 언급하고 포스팅을 마치려 한다.

 

Request <--> Controller <--> Service <--> Response(DB)

 

Controller는 Request 데이터를 적절히 가공해서 Service에 전달하는 역할만 한다.

Service는 가공된 데이터를 가지고 원하는 로직을 적용한 뒤 DB에 요청을 전달해 결과값을 받아 온다.

그리고 그 과정에서 DTO라는 틀을 통해 데이터를 주고 받는다.

 

Service 단에서 사용하는 로직을 '비즈니스 로직'이라고 한다.

이 '비즈니스 로직'을 Route, Controller에서 분리시켜 재사용, 유지보수가 용이하게 한다.

 

Request 객체를 받아오고 Response 객체를 만들어 전달하는 것을 Controller에서,

그것에 원하는 로직을 적용하는 것은 Service에서 실행하는 것이다.

 


 

이렇게 express 프로젝트를 생성하고 간단하게 모델과 CRUD api를 작성해서 테스트해보았다.

앞으로 node, express, MongoDB를 사용하는 프로젝트가 있다면,

본 포스팅 코드를 기반으로 서버 구현을 시작해도 좋을 것 같다.

 

 

 

반응형

+ Recent posts