기본 REST API 만들기

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

git clone https://github.com/a-mean-blogger/tour-of-heroes-api.git
cd tour-of-heroes-api
git reset --hard 704b511
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/tour-of-heroes-api/tree/704b511112d351f88a9f34403b105b4499f23509


이전 포스트에서는 REST API를 테스트하는 방법에 대해 알아보았습니다. 이번에는 Node JS를 사용해서 REST API를 직접 만들어 봅시다.

이 강의는 node js, Express로 기초적인 사이트를 제작할 수 있는 분들을 대상으로 합니다. MEAN Stack/개발 환경 구축Node JS 첫걸음/Hello World! 및 Node JS 첫걸음/주소록 만들기를 공부하신 후에 이 포스트를 진행하시기 바랍니다.

Angular 2로 만들었던 Tour of Heroes/HTTP 예제에 사용할 수 있는 API를 만들어 볼 텐데요, 이 강의 자체는 Angular 2나 Tour of Heroes 예제를 공부하지 않았더라도 진행할 수 있습니다. Angular는 프론트엔드 개발을 위해서, API는 백엔드 개발을 위해서 각각 독립적으로 사용되기 때문입니다.

API 스펙

Tour of Heroes 사이트는 hero의 리스트를 보여주고(index), 하나의 hero의 정보를 보여주고(show), hero를 생성하고(create), hero의 정보를 수정하고(update), hero를 삭제(destroy)하는 5가지 API를 요구합니다. API를 만들기 전에 스펙(Specifications)을 먼저 고안합시다.

실제로는 고객으로부터 스펙을 받은 다음(혹은 스펙을 만들어 고객으로부터 확인을 받은 다음) 해당 스펙의 조건을 충촉하는 API를 만들게 됩니다.

1. Index

  • Endpoint: (GET) api/heroes?name={term}
  • Description: hero들의 목록을 id 순으로 리턴합니다. name 파라메터는 옵션입니다. name 파라메터가 있으면 hero의 name에 {term}이 포함되어 있는 hero들의 목록을 리턴합니다.
  • Request Example 1:
    Url: (GET) api/heroes
    Body: N/A
  • Response Example 1:
    {
        "success": true,
        "data": [
            {
                "_id": "5984f2a0d8afc1040c004ee9",
                "id": 1,
                "name": "My Hero 1",
                "__v": 0
            },
            {
                "_id": "5984f2c6d8afc1040c004eea",
                "id": 2,
                "name": "My Hero 2",
                "__v": 0
            },
            {
                "_id": "5984f2cdd8afc1040c004eec",
                "id": 3,
                "name": "Your Hero 1",
                "__v": 0
            }
        ]
    }
  • Request Example 2:
    Url: (GET) api/heroes?name=my
    Body: N/A
  • Response Example 2:
    {
        "success": true,
        "data": [
            {
                "_id": "5984f2a0d8afc1040c004ee9",
                "id": 1,
                "name": "My Hero 1",
                "__v": 0
            },
            {
                "_id": "5984f2c6d8afc1040c004eea",
                "id": 2,
                "name": "My Hero 2",
                "__v": 0
            }
        ]
    }

 

2. Show

  • Endpoint: (GET) api/heroes/{id}
  • Description: id가 {id}인 hero를 리턴합니다.
  • Request Example:
    Url: (GET) api/heroes/2
    Body: N/A
  • Response Example:
    {
        "success": true,
        "data": {
            "_id": "5984f2c6d8afc1040c004eea",
            "id": 2,
            "name": "My Hero 2",
            "__v": 0
        }
    }

 

3. Create

  • Endpoint: (POST) api/heroes/{id}
  • Description: {name:String}을 받아 새로운 hero를 생성합니다. hero의 id는 현재 마지막 hero id의 다음 번호입니다. 새로생성된 hero를 리턴합니다.
  • Request Example:
    Url: (POST) api/heroes
    Body: 
    {
        "name": "Your Hero 2"
    }
  • Response Example:
    {
        "success": true,
        "data": {
            "__v": 0,
            "id": 4,
            "name": "Your Hero 2",
            "_id": "5984f935ed01e00d54553d36"
        }
    }

 

4. Update

  • Endpoint: (PUT) api/heroes/{id}
  • Description: {id:Number,name:String}를 받아 id가 {id}인 hero를 수정합니다.
  • Request Example:
    Url: (PUT) api/heroes/4
    Body:
    {
        "id": 4,
        "name": "My Hero 3"
    }
  • Response Example:
    {
        "success": true
    }

 

5. Destroy

  • Endpoint: (DELETE) api/heroes/{id}
  • Description: id가 {id}인 hero를 삭제합니다.
  • Request Example:
    Url: (DELETE) api/heroes/4
    Body: N/A
  • Response Example:
    {
        "success": true
    }

프로젝트 생성 및 Package 설치

프로젝트 생성에 앞서, 해당 프로젝트에서 사용할 DB를 생성하고 환경변수에 등록해 줍시다.

프로젝트 폴더를 생성합니다. 해당 폴더에서 command line(cmd, git bash 등)에 npm init을 입력하여 node.js 프로젝트를 생성합니다.

$ npm init

프로젝트에 필요한 package들도 설치해줍니다.

$ npm install express mongoose body-parser --save

이번 강의에서 필요한 package는 express, mongoose, body-parser입니다. 각각의 package에 대해 잠깐 복습을 해보면,

  • express: node JS 웹서버 제작 프레임워크
  • mongoose: mongoDB 라이브러리
  • body-parser: HTTP의 body를 오브젝트로 변환시켜 주는 middleware

프로젝트 생성과정과 위 각각의 package들이 이해가 잘 안되시는 분들은 반드시 MEAN Stack/개발 환경 구축부터 Node JS 첫걸음/주소록 만들기까지 다시 한번 읽어 주시기 바랍니다.

폴더 구조


웹사이트를 만들 땐 routes 폴더가 있었는데 API는 api 폴더가 그 역할을 합니다. API와 일반사이트를 섞어 만들때 구별하기 쉽게 하기 위함이죠. 웹사이트를 만들 때 사용했던 views나 public등의 폴더도 없습니다.

코드

// server.js

var express    = require('express');
var app        = express();
var path       = require('path');
var mongoose   = require('mongoose');
var bodyParser = require('body-parser');

// Database
mongoose.Promise = global.Promise;
mongoose.connect(process.env.MONGO_DB, {useMongoClient: true});
var db = mongoose.connection;
db.once('open', function () {
   console.log('DB connected!');
});
db.on('error', function (err) {
  console.log('DB ERROR:', err);
});

// Middlewares
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(function (req, res, next) { //1
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'content-type');
  next();
});

// API
app.use('/api/heroes', require('./api/heroes'));

// Server
var port = 3000;
app.listen(port, function(){
  console.log('listening on port:' + port);
});

1. 보안상의 이유로 서버는 기본적으로 같은 서버가 아닌 다른 곳에서 오는 요청들을 기본적으로 차단합니다. 또한 클라이언트에서 오는 요청도 다른곳으로 간주합니다. 하지만 API는 클라이언트를 위한 프로그램이므로 같은 서버가 아닌 다른 곳에서 오는 요청들을 허가해야 하는데, 이것을 HTTP 접근 제어 혹은 CORS(Cross-origin resource sharing, 출처가 다른 곳끼리 자원 공유)라고 합니다.

  • Access-Control-Allow-Origin: 요청이 허용되는 url을 route을 제외하고 적습니다. 이외의 url로 부터 오는 요청은 거절됩니다. 단 *은 모든 요청을 허가시킵니다.
  • Access-Control-Allow-Methods:요청이 허용되는 HTTP verb 목록을 적습니다. 여기에 포함되지 않은 HTTP verb의 요청은 거절됩니다. *을 사용할 수 없습니다.
  • Access-Control-Allow-Headers: 요청이 허용되는 HTTP header 목록을 적습니다. 여기에 포함되지 않은 HTTP header는 사용할 수 없습니다.  *을 사용할 수 없습니다.

Node JS 첫걸음/주소록 만들기의 최종 코드와 비교해서 view에 관련된 부분이 모두 제거되고 CORS가 추가되었습니다.

// models/hero.js

var mongoose = require('mongoose');

var heroSchema = mongoose.Schema({
  id: {
    type: Number,
    required: true,
  },
  name: {
    type: String,
    required: true,
  },
});

var Hero = mongoose.model('hero', heroSchema);
module.exports = Hero;

Tour of Heroes에서 사용되는 hero의 schema입니다. 숫자로 된 id와 문자열인 name을 가지고 있습니다.

// api/heroes.js

var express  = require('express');
var router   = express.Router();
var Hero     = require('../models/hero');
var mongoose = require('mongoose');

// Index
router.get('/',
  function(req, res, next){
    var query = {};
    if(req.query.name) query.name = {$regex:req.query.name, $options:'i'}; //1

    Hero.find(query)
    .sort({id: 1})
    .exec(function(err, heroes){
      if(err) {
        res.status(500);
        res.json({success:false, message:err});
      }
      else {
        res.json({success:true, data:heroes});
      }
    });
  }
);

// Show
router.get('/:id',
  function(req, res, next){
    Hero.findOne({id:req.params.id})
    .exec(function(err, hero){
      if(err) {
        res.status(500);
        res.json({success:false, message:err});
      }
      else if(!hero){
        res.json({success:false, message:"hero not found"});
      }
      else {
        res.json({success:true, data:hero});
      }
    });
  }
);

// Create
router.post('/',
  function(req, res, next){
    Hero.findOne({})
    .sort({id: -1})
    .exec(function(err, hero){
      if(err) {
        res.status(500);
        return res.json({success:false, message:err});
      }
      else {
        res.locals.lastId = hero?hero.id:0;
        next();
      }
    });
  },
  function(req, res, next){
    var newHero = new Hero(req.body);
    newHero.id = res.locals.lastId + 1;
    newHero.save(function(err, hero){
      if(err) {
        res.status(500);
        res.json({success:false, message:err});
      }
      else {
        res.json({success:true, data:hero});
      }
    });
  }
);

// Update
router.put('/:id',
  function(req, res, next){
    Hero.findOneAndUpdate({id:req.params.id}, req.body)
    .exec(function(err, hero){
      if(err) {
        res.status(500);
        res.json({success:false, message:err});
      }
      else if(!hero){
        res.json({success:false, message:"hero not found"});
      }
      else {
        res.json({success:true});
      }
    });
  }
);

// Destroy
router.delete('/:id',
  function(req, res, next){
    Hero.findOneAndRemove({id:req.params.id})
    .exec(function(err, hero){
      if(err) {
        res.status(500);
        res.json({success:false, message:err});
      }
      else if(!hero){
        res.json({success:false, message:"hero not found"});
      }
      else {
        res.json({success:true});
      }
    });
  }
);

module.exports = router;

우선 모든 route이 json을 return한다는 것을 눈여겨 보시기 바랍니다. 사실 웹사이트와 API의 차이점은 html을 return하지 않고 데이터를 바로 리턴한다는 것 뿐입니다. 1번의 regex를 사용하는 부분을 제외하면 이전까지의 강의와 비교해서 특별한 것이 없는 코드입니다. 만약 이해가 안되시는 부분이 있다면 개별질문주세요.

실행 결과

Node JS API/API 테스트 프로그램(Postman)설치및 간단 사용법에서 살펴봤던 포스트맨(Postman)을 사용해서 API를 테스트 해봅시다. 해당 포스트에서는 POST용 API가 없어서 연습을 제대로 하지 못했는데, 이번에는 제대로 해봅시다.

nodemon을 사용해서 서버를 실행합시다

nodemon

Postman을 실행하고 hero를 만들어 봅시다. Method는 POST를 선택하고 url에 http://localhost:3000/api/heroes를 입력합니다. Body를 눌러 raw를 선택하고 우측 dropdown 메뉴에서 JSON을 선택합니다. 아래 텍스트 입력창에 다음과 같이 입력합니다.

{
  "name": "My Hero 1"
}

이후 send를 눌러서 "success": true가 보인다면 성공입니다.

Tour of Heroes API TEST Create

저는 My Hero 5번까지 5개의 hero를 만들었습니다.

이후 새 탭을 열고 GET http://localhost:3000/api/heroes 하면 아래와 같이 목록을 볼 수 있습니다.

Tour of Heroes API TEST Index

나머지 API들도 스펙에 맞게 잘 작동하는지 테스트해 봅시다.

마치며..

강의 본문에서 언급하였지만 사실 웹사이트와 API의 개발에서의 차이는 data를 어떠한 형식으로 제공하는지의 차이밖에 없습니다. MEAN stack에서 실제 사이트 개발은 Angular로 하는데 이 블로그에서 Node JS/Express 강의들(주소록 만들기, 게시판 만들기)에 상당한 분량이 들어 간 것도 이 때문입니다.

이 API가 Angular에서 어떻게 활용되는지가 궁금하신 분들은 Angular 2 카테고리의 글들을 읽어보세요.

긴 글 읽으시느라 수고하셨고 질문있으면 댓글로 남겨주세요.

댓글

김남현 2017.10.26
잘 봤습니다. 로그인 기능이 페이스북으로 제공되니 좋네요 :)
I
Ian H 2017.10.27
@김남현,
감사합니다 :)
C
Changjin Lee 2018.03.04
1번  $regex 설명 부탁드려요. 놓친 부분이 있는 것 같은데 어떻게 작동하는지 잘 모르겠습니다.
I
Ian H 2018.03.05
@Changjin Lee,
query.name = {$regex:req.query.name, $options:'i'}이고 Hero.find(query)  이니까 결국 Hero.find({name:{$regex:req.query.name, $options:'i'}})인 셈이죠.
즉 name이 {$regex:req.query.name, $options:'i'}인 hero를 찾는 것이고,
regex를 사용해서 name이 query로 들어온 name(req.query.name)과 일치하는 hero를 찾는 것입니다. $options:'i'는 대소문자 구별을 무시하는 옵션을 추가하는 것입니다.
J
JaeHwan Kim 2018.06.20
미쳤다....너무 좋습니다 ㅠㅠㅠㅠ감사드립니다!!!
I
Ian H 2018.06.26
@JaeHwan Kim,
감사합니다^^
G
Goeun Choi 2018.09.27
안녕하세요! HttpClient쪽에서 아래와 같이 함수를 setting 해주어야 합니다. 그럼 서버단에서도 이의 변수를 맞춰줘야 하는거 맞나요?? HttpClient_initRequest(Ip_Address_T *addr_ptr, Ip_Port_T port, Msg_T **msg_pptr);
I
Ian H 2018.09.27
@Goeun Choi,
http://xdk.bosch-connectivity.com/xdk_docs/html/_serval___http_client_8h.html 이 내용 맞나요?
어쨌든 REST API에서는 client와 server의 변수를 맞추는 개념은 아닙니다. 데이터를 HTTP 규격에 맞게 요청하셔야 되요. 데이터는 query string이나 http body나 http header에 넣어서 정보교환을 합니다.
정재표 2018.11.22
잘보고있습니다
I
Ian H 2018.11.22
@정재표,
감사합니다^^
류동기 2019.03.08
안녕하세요. post를 할때, json 형식을  {     "id": "8",     "name": "sdfwfef",     "value": "235555" } 로 하면 잘 들어가는데,  [   {     "id": "8",     "name": "sdfwfef",     "value": "235555"   },   {     "id": "9",     "name": "dsfdssdfwfef",     "value": "9951"   } ]
이렇게 두개를 한번에 post하려면 어느부분을 어떻게 수정해야 할까요?? value는 제가 임의로 바꾼 부분입니다.
I
Ian H 2019.03.08
@류동기,
입력받은 json data는 req.body에 들어갑니다.
api/heroes/{id}는 API스펙에 따라 하나의 객체만을 받으므로 var newHero = new Hero(req.body);로 바로 새로운 hero객체를 만드는데요, json으로 배열을 받는 경우, req.body에 배열이 들어가게 됩니다. 즉 new Hero(req.body)로 바로 hero객체를 생성할 수 없게 되는 거죠.
이 부분을 고치면 됩니다. 먼저 직접 코드 수정을 해보시고 안되시면 또 알려주세요^^
류동기 2019.03.11
@Ian H,
function(req, res, next) {     var newHero = new Hero(     [         {         "id": "1111111111111111111",             "name": "sdffggfwfef",             "value": "235555"         },         {             "id": "5665566",             "name": "sdffggfwfef",             "value": "235555"   }]);     //newHero.id = res.locals.lastId + 1;     newHero.save(function(err, hero) {         if(err) {             res.status(500);             res.json({success: false, message: err});         }         else {             res.json({success: true, data: hero});         }     }); } );
이런식으로 req.body부분에 처음부터 데이터를 적어서 보내는데, 한개는 잘가는데 두개부터는 잘안됩니다.. 위에 function부분은 주석처리했습니다. 또 다른부분에 고쳐야할 부분이 있을까요?
I
Ian H 2019.03.11
@류동기,
new Hero에는 배열이 아닌 하나의 객체만 들어가야 합니다. 만약 배열로 new Hero를 생성하려면, 배열을 loop하면서 하나씩 처리해 주시면 되겠습니다^^
민유영 2019.06.19
 안녕하세요! nodemon을 실행하면 계속 아래와 같은 에러가 뜹니다.. events.js:174       throw er; // Unhandled 'error' event       ^
Error: Invalid mongodb uri "mongodb+srv://test_user:[email protected]/test?retryWrites=true&w=majority". Must begin with "mongodb://"     at muri (C:\Users\MyHome\MyProject\contactbook\contact-book\tour-of-heroes-api\node_modules\muri\lib\index.js:28:11)     at NativeConnection.Connection.openUri (C:\Users\MyHome\MyProject\contactbook\contact-book\tour-of-heroes-api\node_modules\mongoose\lib\connection.js:766:18)     at Mongoose.connect (C:\Users\MyHome\MyProject\contactbook\contact-book\tour-of-heroes-api\node_modules\mongoose\lib\index.js:262:17)     at Object.<anonymous> (C:\Users\MyHome\MyProject\contactbook\contact-book\tour-of-heroes-api\server.js:12:10)     at Module._compile (internal/modules/cjs/loader.js:776:30)     at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)     at Module.load (internal/modules/cjs/loader.js:653:32)     at tryModuleLoad (internal/modules/cjs/loader.js:593:12)     at Function.Module._load (internal/modules/cjs/loader.js:585:3)     at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)     at startup (internal/bootstrap/node.js:283:19)     at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3) Emitted 'error' event at:     at NativeConnection.Connection.error (C:\Users\MyHome\MyProject\contactbook\contact-book\tour-of-heroes-api\node_modules\mongoose\lib\connection.js:673:8)     at NativeConnection.Connection.openUri (C:\Users\MyHome\MyProject\contactbook\contact-book\tour-of-heroes-api\node_modules\mongoose\lib\connection.js:775:10)     at Mongoose.connect (C:\Users\MyHome\MyProject\contactbook\contact-book\tour-of-heroes-api\node_modules\mongoose\lib\index.js:262:17)     [... lines matching original stack trace ...]     at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3) [nodemon] app crashed - waiting for file changes before starting...
구글에 검색해보고 mongoose 서버를 닫았다가 다시 열어보기도 하고 mongodb+srv를 mongodb로 바꿔보기도 했는데도 에러가 고쳐지지를 않아서 혹시 해결방법을 아실까해서 댓글 남깁니다..!
I
Ian H 2019.06.19
@민유영,
구글에 검색해보니 mongoose를 최신버전으로 업데이트하면 고쳐진다고 하네요. 한번 해보세요^^
https://stackoverflow.com/questions/48980473/invalid-mongodb-uri-must-begin-with-mongodb
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP