Medium - 5 Steps to build a REST API in Node.js with MongoDB

원문 - Daniel Deutsch

Trend 파악을 Medium 기고문 요약 포스팅 - MongoDB와 Node.js에서 REDT API를 만드는 5가지 단계

https://unsplash.com/photos/th3rQu0K3aM

➡️ Github Repo is available here ⬅️

우리는 우리의 고객이 파티에 초대된 게스트들이고 우리가 호스트인걸 알고 있습니다. 매일 고객의 경험을 조금이라도 낫게 만들 수 있도록 하는 것이 우리의 매일 과업입니다. - Jeff Bezos

Rest API?

REST API는 웹 어플리케이션에서 서버 쪽을 처리합니다. 이말은 백엔드는 한번 빌드되면 컨텐트나 데이터를 프론트엔드, 모바일, 다른 서버쪽 앱에다 제공합니다. google calendar API가 좋은 예제입니다.

REST는 REpresentational State Transfer(대표적인 상태 전달;네트워크 아키텍쳐 원리의 모음)를 의미하고 웹서버가 요청에 응답해야하는 방법입니다. 이것은 데이터를 읽는 방법 외에도 업데이트, 삭제, 데이터 생성과 같은 것을 말합니다.

여기서 만들 API는 질문을 만들고 답변을 생성, 수정, 삭제, 추천할 수 있습니다. API 프로그래밍의 핵심은 적절한 라우터 구조입니다. 그래서 다음과 같은 프로그램 라우터가 필요합니다.

  • 존재하는 질문을 찾아보고 우리의 질문을 만드는 것
  • 답변을 읽고 만들고 수정하고 삭제하는 것
  • 답변 추천

    What I will use for this introduction

    Development Environment

    여기서는 일반 자바스크립트와 다음의 도구를 사용합니다.

  • Node.js
  • Express (JS framework)
  • MongoDB (Database)
  • Yarn (package management)
  • Visual Studio Code(에디터용)
  • Postman (API 테스트)

    Dependencies

    다음과 같은 패키지들이 사용됩니다.

  • body-parser (들어온 요청을 파싱합니다)
  • express (어플리케이션을 동작하게 만듭니다)
  • nodemon (변경이 있을 경우 서버를 재시작합니다)
  • mongoose (MongoDB와 쉽게 상호작용하기 위한 객체 데이터 모델링입니다.)
  • morgan (HTTP 요청 기록 미들웨어)
  • eslint with Arirbnb extension (수준 높은 코드를 작성하기 위함)

    Structure

    이 예제는 다음과 같은 구조를 가집니다.

  • express로 API 라우터 구성
  • API 데이터 모델링
  • Mongoose를 이용해서 Mongo와 통신
  • API 테스트 및 마무리

    Building the API routes

    Set up basics

  • 개발을 단순화하고 스크립트를 package.json에 적절히 추가하기 위해 nodemon 패키지를 설치합니다.
  • express app의 기본 웹서버로 시작합니다. (express 예제 문서를 확인하세요)
  • 가능한 유연성을 확보하기 위해 express를 미들웨어로 사용합니다.
  • 요청을 파싱하기 위해 body-parser 패키지를 추가합니다.
  • 미들웨어에서 파서를 사용합니다.

    Create question routes

  • router.js 파일을 만들고 모든 경로를 저장합니다.
  • 질문을 찾고 만들기 위해 GET/POST 경로를 설정합니다.
  • 특정 질문을 위한 GET 경로를 설정합니다.
  • 예제 :
    router.get('/', (req, res) => {
    res.json({ response: 'a GET request for LOOKING at questions '})
    });
    router.post('/', (req, res) => {
    res.json({
      response: 'a POST request for CREATING questions',
      body: req.body
      });
    });
    router.get('/:qID', (req, res) => {
    res.json({
      response: `a GET request for LOOKING at a special answer id: ${rew.params.qID}`
      });
    });
    

    Create answer routes

  • http 요청을 기록하기 위한 morgan 패키지를 설치합니다 (콘솔에서 리퀘스트를 분석하는데 도움이 됩니다.)
  • 답변을 생성하는 POST 경로를 설정합니다.
  • 답변을 수정하고 삭제하는 PUT/DELETE 경로를 설정합니다.
  • 답변에 추천을 하는 POST 경로를 설정합니다.
  • 예제:
    router.post('/:qID/answers', (req, res) => {
    res.json({
      response: 'a POST request for CREATING answers',
      question: req.params.qID,
      body: req.body
      });
    });
    router.put('/:qID/answers/:aID', (req, res) => {
    res.json({
      response: 'a PUT request for EDITING answers',
      question: req.params.qID,
      answer: req.params.aID,
      body: req.body
      });
    });
    router.delete('/:qID/answers/:aID', (req, res) => {
    res.json({
      response: 'a DELETE request for DELETING answers',
      question: req.params.qID,
      answer: req.params.aID,
      body: req.body
      });
    });
    router.post('/:qID/answers/LaID/vote-:dec', (req, res) => {
    res.json({
      response: 'a POST request for VOTING on answers',
      question: req.params.qID,
      answer: req.params.aID,
      vote: req.params.dec,
      body: req.body
      });
    });
    

    Set up error handlers

  • 에러를 효과적으로 잡기위해 미들웨어를 사용
  • 404에러를 캐치하여 커스텀 에러핸들러를 읽을 수 있는 JSON 메시지로 전달함(에러가 없으면 500을 사용)
  • 추천 에러를 처리하기 위해 개별 유효성 미들웨어를 만듦(up/down만 허용)
  • 예제
    app.use((req, res, next) => {
    const err = new Error('Not Found');
    err.status = 404;
    next(err);
    });
    app.use((err, req, res, next) => {
    res.status(err.status || 500);
    res.json({
      error: {
        message: err.message
      }
      });
    });
    

Connecting with MongoDB through Mongoose

Modeling data for the API

구조는 데이터베이스에 어떤 종류의 데이터가 저장될 지와 연관된 데이터의 타입이 어떤 것인지 나타냅니다. MongoDB의 데이터 처리를 위해 몽구스를 사용할 것입니다. 스키마들은 JSON 형태로 데이터를 정의하는 것을 허용합니다.

이 경우 가장 좋은 구현은 답변 프로퍼티를 가진 질문 객체를 사용하는 것입니다. 그리고 문서에는 저장한계가 있기 때문에 답변의 수가 제한된다는 것을 명심하세요.

Creating the schema

  • 여러분이 원하는 부모-자식 구조에 따라 스키마를 만드세요
  • 스키마로 모델을 만듭니다.
  • 예제
const AnswerSchema = new Schema({
  text: String,
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now },
  votes: { type: Number, default: 0}
  });
const QuestionSchema = new Schema({
  text: String,
  createAt: { type: Date, default: Date.now },
  answers: [AnswerSchema]
  });

const Question = mongoose.model('Question', QuestionSchema)

Extend the functionality by sorting and voting

  • 최신 답변이 위에 오도록 정렬되어야 합니다.
  • 추천은 데이터베이스에 저장되어야 합니다.
  • 저장할 때 정렬하기 위해 몽구스 프리훅을 만듭니다.
  • 부모의 문서를 조회하기 위해 자식 문서의 부모 메소드를 호출합니다. (답변은 질문을 조회합니다)
  • 이것을 구현할 때 화살표 함수는 안되는 것을 명심하세요 (화살표 함수의 this는 블록 내로 제한되므로 메소드에 사용하는게 적절하지 않음)
  • 예제
const sortAnswers = (a, b) => {
  if (a.votes === b.votes) {
    return b.updatedAt - a.updatedAt;
  }
  return b.votes - a.votes
};
QuestionSchema.pre('save', function (next) {
  this.answers.sort(sortAnswers);
  next();
  });
AnswerSchema.method('update', function (updates, callback) {
  Object.assign(this, updates, { updatedAt: new Date() });
  this.parent().save(callback);
  });
AnswerSchema.method('vote', function (vote, callback) {
  if ( vote === 'up') {
    this.votes += 1;
  } else {
    this.votes -= 1;
  }
  this.parent().save(callback);
  });

Connecting the API the database

❗저에겐 이 파트가 가장 어려웠습니다. 처음부터 잘 안돼더라도 너무 낙심하지 마세요. 몽구스 문서를 적절하게 공부를 하세요 :)

Establish error handling

  • 라우터의 param 메소드를 사용해서 특정 라우터의 콜백을 유발시킵니다 (qID와 aID 사용) see docs
  • 이 방법으로 여러분은 질문이나 답변을 찾을 수 없을 때 발생하는 에러를 항상 체크할 수 있습니다.
  • 예제
router.param('qID', (req, res, next, id) => {
  Question.findById(id, (err, doc) => {
    if (!doc) {
      err = new Error('Document not found');
      err.status = 404;
      return next(err);
    }
    req.question = doc;
    return next();
    });
  });
router.param('aID', (req, res, next, id) => {
  req.answer = req.question.answers.id(id);
  if (!req.answer) {
    err = new Error('Answer not found');
    err.status = 404;
    return next(err);
  }
  return next();
  });

Update question routes

  • 데이터베이스에서 질문을 찾는 GET route에 사용
  • 질문들을 리턴
  • 요청의 본문으로 새로운 질문을 만들고 데이터베이스에 JSON 형태로 저장하는 POST route 사용
  • 특정 질문에 방금 응답한 하나의 질문을 위한 GET route
  • 예제
router.get('/', (req, res, next) => {
  Question.find({}).sort({ createdAt: -1}).exec((err, questions) => {
    if (err) return next(err);
    res.json(questions);
    });
  });
router.post('/', (req, res) => {
  const question = new Question(req.body);
  question.save((err, question) => {
    if (err) return next(err);
    res.status(201);
    res.json(question);
    });
  });
router.get('/:qID', (req, res) => {
  res.json(req.question);
  });

Update answer routes

  • 단순히 답변을 질문 문서의 답변 배열에 넣고 몽구스의 JSON 메소드를 이용해서 저장하는 POST route
  • update 메소드로 새로운 JSON을 리턴하는 PUT route
  • removed 메소드를 사용하는 DELETE route
  • 미들웨어를 사용해 정확한 표현인지 검사하고 모델파일에서 정의한 vote 메소드를 사용하는 POST route
  • 예제
router.post('/LqID/answer', (req, res, next) => {
  req.question.answers.push(req.body);
  req.question.save((err, question) => {
    if (err) return next(err);
    res.status(201);
    res.json(question);
    });
  });
router.put('/:qID/answers/:aID', (req, res, next) => {
  req.answer.update(req.body, (err, result) => {
    if (err) return next(err);
    res.json(result);
    });
  });
router.delete('/:qID/answers/:aID', (req, res) => {
  req.answer.remove(err => {
    req.question.save((err, question) => {
      if (err) return next(err);
      res.json(question);
      });
    });
  });
router.post(
  '/:qID/answers/:aID/vote-:dec',
  (req, res, next) => {
    if (req.params.dec.search(/^(up|down)$/) === -1) {
      const err = new Error(`Not possible to vot for ${req.params.dec}!`);
      err.status = 404;
      next(err);
    } else {
      req.vote = req.params.dec;
      next();
    }
  },
  (req, res, next) => {
    req.answer.vote(req.vote, (err, question) => {
      if (err) return next(err);
      res.json(question);
      });
    }
  );

이 시점에서부터 REST API가 완전히 구현된 것이고 사용될 준비가 된 것입니다.

Finalizing and testing the API

Testing

모든 경로들이 의도대로 제대로 동작하는지 확인하기 위해서 Postman의 크롬확장을 사용할 것입니다. 인터페이스가 모든 HTTP 메소드를 효과적으로 테스트하는데 도움이 됩니다. 한가지 팁을 드리자면 모든 경로를 수동으로 테스트하는 것 대신에 자동화 테스트도 설정할 수 있습니다.

CORS

Cross Origin Resource Sharing은 브라우저가 다른 도메인의 리소스 헤더에 접근할 수 있게 해줍니다. 보안의 위험때문에 이것은 제한적입니다. 우리는 API를 소비하기 위한 도메인을 위해 미들웨어를 만들 것입니다.

  • 모든 origin에 접근할 수 있도록 헤더를 설정합니다.
  • HTTP 메소드를 허용합니다.
  • 예제
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header(
    'Access-Control-Allow-Headers',
    'Origin, X-Requested-With, Content-Type, Accept'
    );
    if (req.method === 'Options') {
      res.header('Access-Control-Allow-Methods', 'PUT, POST, DELETE');
      return res.status(200).json({});
    }
  });

https://unsplash.com/photos/PJCZOWuOxbU

Connecting the Frontend

작업 API는 모든 타입의 프론트엔드 프레임워크와 연결될 수 있습니다. ➡️ Github Repo is available here ⬅️

Conclusion

위에서 보았듯이 REST API는 기본 백엔드 마이크로서비스를 설정하는데 좋은 도구입니다. 실제로 API를 구현하는 것은 작업하는 데이터베이스에 대한 완벽한 이해가 필요하고 route와 어떻게 상호작용하는지 알아야합니다. 여기서는 몽구스를 썼지만요,

Summary

  • Node.js와 MongoDB(with Mongoose)를 사용한 REST API 예제
  • REST API는 대표적인 상태를 전송하는 API로 요청의 상태에 따른 REpresentational을 넘겨준다
  • REpresentational은 값보다 상위 개념으로 doc과 같이 더욱 많은 정보를 포함한다.
  • REST API 테스트에서는 모든 route에 대해 확인을 해야하며 Postman을 주로 사용한다. 자동화 테스트 설정도 가능하다.

© 2019. All rights reserved.

Powered by Hydejack v8.1.1