BE_[Node] 4. Express와 TypeORM을 활용한 CRUD API 만들기(2)_11.2

송철진·2022년 11월 2일
0
post-custom-banner

2. Express와 TypeORM이 적용된 CRUD API 만들기

TypeORM에서 제공하는 연결(Pooling) 기능을 사용하여, 데이터베이스에 Create, Read, Update, Delete 작업을 수행할 수 있는 API로 발전시켜 작은 프로젝트 앱을 완성하자

2-1. 초기 환경 세팅 (생략)

흰색박스는 비밀번호이다.
crud_express는 생성해놓은 데이터베이스 이름이다

👉나중에 알았지만 TYPEORM_DATABASE = westagram crud_express

.env.sample 파일
: 상단에 database_url을 작성한다
DATABASE_URL = "mysql://username:password@127.0.0.1:3306/database_name"


  1. 최상위 상위 root 디렉토리에서 db 라는 폴더를 생성
    : dbmate가 관리하는 migrations 파일들과 schema.sql 을 보관하는 역할

db라는 명칭
dbmate가 기본으로 migration/shema 파일이 저장되도록 설정한 디렉토리 명칭이다. 별도의 네이밍을 원한다면 공식문서를 참고하여 스스로 설정할 것.

dbmate 전역으로 설치: npm install -g dbmate
👉 로컬로 설치하면 command not found 오류 발생!

  1. dbmate 고유의 테이블 생성 명령어 입력
$ dbmate new create_books_table
$ dbmate new create_authors_table
$ dbmate new create_book_authors_table

tree커맨드 설치: brew install tree
tree로 db에 생성된 파일 확인하기 tree db

  • 마이그레이션파일의 생성날짜 확인 가능.

각 테이블.sql파일의 구조

2-2. 데이터베이스 테이블 생성

  1. 테이블을 생성합니다
  • 책 테이블을 생성하고, 내부 컬럼 필드, 데이터 속성을 정의하는 SQL문

  • 작가의 테이블을 정의하는 SQL문

  • 작가와 책을 연결하는 중간테이블을 정의하는 SQL문

-- migrate:up
테이블의 각 요소들을 정의한다.
-- migrate:down
down명령을 쓰면 모든 box의 테이블을 drop시켜 데이터베이스에서 삭제한다.

  1. migration 파일들을 직접 테이블 옮기는 migrate 작업 dbmate up

    schema.sql
    : migration이 정상 작동하며 데이터베이스에 준 모든 변화를 기록

2-3. CRUD API 작성하기

기본 소스코드

// app.js
const http = require("http");
const express = require("express");
const cors = require("cors");
const morgan = require("morgan");
const dotenv = require("dotenv");
dotenv.config();

const {appendFile} =  require("fs")

const { DataSource } = require("typeorm");

const myDataSource = new DataSource({
    type: process.env.TYPEORM_CONNECTION,
    host: process.env.TYPEORM_HOST,
    port: process.env.TYPEORM_PORT,
    username: process.env.TYPEORM_USERNAME,
    password: process.env.TYPEORM_PASSWORD,
    database: process.env.TYPEORM_DATABASE
})

myDataSource.initialize()
    .then(() => {
        console.log("Data Source has been initialized!")
    })
    .catch((err) => {
        Console.error("Error during Data Source initialization", err)
    myDataSource.destroy()
    })

const app = express();
const PORT = process.env.PORT;

app.use(cors());
app.use(morgan('dev')); 
app.use(express.json());

// health check
app.get("/ping", (req,res) => {
  	// 201을 200으로 바꿔서 통신되는지 확인하기!
    res.status(201).json({ message : "pong" });
})
/*

앞으로
생성, 수정, 조회, 삭제 에 대한 구현을 할 것이다!

*/

const server = http.createServer(app);

const start = async () => {
    try{
        app.listen(PORT, () => console.log(`Server is listening on ${PORT}`));
    } catch (err) {
        console.error(err);
    }
}
start();

상태코드 200 <-> 201 비교하기

2-3-1. [C] 책 정보 생성 엔드포인트 구현

// create a book
app.post("/books", async (req, res, next) => {
    const { title, description, coverImage } = req.body

    // console.log(req)

    await myDataSource.query(
        `INSERT INTO books(
            title, 
            description,
            cover_image
        ) VALUES (?, ?, ?);
        `,
        [ title, description, coverImage ]
    );

    res.status(201).json({ message : "successfullty created" });
})

설명

  • "/books"(endpoint 타겟 리소스)를 받아서 post(http 메소드)로 들어온 통신을 async(비동기함수)로 동작시킬 수 있게 arrow function화.
  • const로 정의한 title, description, coverImage에
    요청해서 들어온 body값들(req.body)을 받아들여 구조분해 할당을 받아
    변수처럼 활용해서 썼다
  • 비동기함수가 올바른 작동을 해서 비동기 액션을 취할 시,
    await내용이 다시 작동을 한다
  • sql raw query문을 동작하기위해서 myDataSource.query()
    • myDataSource: typeorm에서 설정한 변수명
    • query(): 쿼리를 쓸 수 있게 구조화
  • title, description, coverImage를 가져오기 위해 위에서 선언한 변수명을 대괄호[ ] 안에 VALUES(?, ?, ?)의 물음표? 순서에 맞게 매칭되어 들어가게끔 잘 정의한다
  • 위의 내용이 성공하면 상태코드와 JSON메시지를 보내준다

실행:

$ http -v POST 127.0.0.1:3000/books title="test title" description="test description" coverImage="test coverImage.url"

실행결과:

에러 모음 #3
에러 모음 #4

2-3-2. [R] 모든 책 정보 조회 엔드포인트 구현

// Get all books
app.get('/books', async (req, res) => {
    await myDataSource.manager.query(
        `SELECT
                b.id,
                b.title,
                b.description,
                b.cover_image
            FROM books b`
        , (err, rows) => {
            res.status(200).json(rows);
        })
});

설명

  • get을 통해 들어와야 발동하는 메소드
  • query문을 받아서 select문이 동작할 수 있게끔 되어 있다
  • manager 클래스로 접근해 sql raw query 형태 메소드를 동작시킬 수 있다(가독성을 위해 manager를 쓰지 않고 그냥 query를 쓰는 것 추천!)

books테이블에 데이터를 2개 더 추가, 실행했을 때:

http -v GET 127.0.0.1:3000/books
  • httpie 특성 상 DB 컬럼 순서와 다르게 알파벳순서로 나열된다

2-3-3. [R] 책 + 작가 정보 조회 엔드포인트 구현

// Get all books along with authors
app.get('/books-with-authors', async (req, rea) => {
    await myDataSource.manager.query(
        `SELECT
                books.id,
                books.title,
                books.description,
                books.cover_image,
                authors.first_name,
                authors.last_name,
                authors.age
            FROM books_authors ba
            INNER JOIN authors ON ba.author_id = authors.id
            INNER JOIN books ON ba.book_id = books.id`
        , (err, rows) => {
                res.status(200).json(rows);
        })
});

설명

  • 중간테이블을 활용해 다대다 관계인 authors, books를 join하고 원하는 컬럼만 뽑아내야 한다

authors 테이블에 test data 1개를 입력하고

books_authors 테이블에도 books와 tables를 잇는 test data 1개 입력

실행:

http -v GET 127.0.0.1:3000/books-with-authors
  • httpie는 response 시, Alphabet order로 자동으로 나열한다
  • 나머지 2권의 책은 작가가 연결되지 않아서 1권만 나옴!

에러모음 #5

2-3-4. [U] 책 정보 수정 엔드포인트 구현

// Update a single book by its primary key
app.patch('/books', async(req, res) => {
    const { title, description, coverImage, bookId } = req.body

    await myDataSource.query(
        `UPDATE books
        SET
            title = ?
            description = ?
            cover_image = ?
        WHERE id = ?
        `,
        [ title, description, coverImage, bookId ]
    );
        res.status(201).json({ message : "successfully updated" })
});

실행

  • = 을 쓸 때 양옆에 띄어쓰기하면 안된다
http -v PATCH 127.0.0.1:3000/books bookId='2' title='new title' description='new description' coverImage='new coverImage'

비교하기

2-3-5. [D] 책 정보 삭제 엔드포인트 구현

:bookId 의 개념
클라이언트 선에서 뒷 부분에 책의 id값을 의도대로 기입할 수 있게 설정하는 방법

// Delete a book
app.delete('/books/:bookId', async(req,res)=>{
    const { bookId } = req.params;

    await myDataSource.query(
        `DELETE FROM books
        WHERE books.id = ${bookId}
        `);
        res.status(200).json({ message : "successfully deleted" }); // status 204의 사용도 권장함
    }); 

설명

  • path파라미터를 받아서(req.params) 파라미터에 정의된 정수값에 해당하는 정보의 pk id를 삭제한다

실행: id=3인 책을 삭제하시오

http -v DELETE 127.0.0.1:3000/books/3

비교하기

근데 삭제 시 상태코드는 204가 추천된다
204는 뒤에 작성한 json메시지가 전달되지 않는 특징이 있다(비워두기)

res.status(204).json();

실행: id=3인 책을 삭제하고 204 코드 확인하시오

에러모음 #6

profile
검색하고 기록하며 학습하는 백엔드 개발자
post-custom-banner

0개의 댓글