[Node.JS] 강좌 10-2편: Express 프레임워크 응용하기 – RESTful API 편


list

이 강좌는 강좌 10-1편과 이어지는 강좌입니다.

강좌를 작성하다가 글이 너무 길어져서 3편으로 나누어 작성한 점 유의해주세요 🙂

10-1. EJS
10-2. Restful API
10-3. Session


 

3. RESTful API

REST 는 Representational State Transfer 의 약자로서,  월드와이드웹(www) 와 같은 하이퍼미디어 시스템을 위한 소프트웨어 아키텍쳐 중 하나의 형식입니다. REST 서버는 클라이언트로 하여금 HTTP 프로토콜을 사용해 서버의 정보에 접근 및 변경을 가능케 합니다. 여기서 정보는 text, xml, json 등 형식으로 제공되는데, 요즘 트렌드는 json이죠.

HTTP 메소드

HTTP/1.1 에서 제공되는 메소드는  여러개가 있는데요,
REST 기반 아키텍쳐에서 자주 사용되는 4가지 메소드는 다음과 같습니다.

  1. GET – 조회
  2. PUT –  생성 및 업데이트
  3. DELETE – 제거
  4. POST – 생성

여기서 잠깐, POST 와 PUT 좀 헷갈리지 않나요? 둘다 생성을 한다면..
어느 상황에 무엇을 써야하는거지?  PUT vs POST, REST API (1ambda Blog) 여기서 궁금증을 해소하세요!

데이터베이스 생성

JSON 기반의 사용자 데이터베이스를 만들어보겠습니다.

Node.js 와 궁합이 잘 맞는 MongoDB를 사용했더라면 좋았겠지만
이 포스트의 초점은 Express 이므로 다음으로 미루도록 하겠습니다.

2016/3/3 EDITED: 미룬 강좌가 작성되었습니다.
[Node.JS] 강좌 11편: Express와 Mongoose를 통해 MongoDB와 연동하여 RESTful API 만들기

data 폴더를 만들고 그 안에 user.json 파일을 생성해주세요:

{
    "first_user": {
        "password": "first_pass",
        "name": "abet"
    },
    "second_user":{
        "password": "second_pass",
        "name": "betty"
    }
}

(보안을 생각한다면 패스워드는 Encrypt를 하거나 Hash 를 쓰는게 좋겠지만 이건 예제니까 PASS!
그 부분은나중에 한번 다루도록 하겠습니다.)

첫번째 API: GET /list

모든 유저 리스트를 출력하는 GET API 를 작성해보겠습니다.

우선, user.json 파일을 읽어야 하므로, fs 모듈을 사용하겠지요?

module.exports = function(app, fs)
{

     app.get('/',function(req,res){
         res.render('index', {
             title: "MY HOMEPAGE",
             length: 5
         })
     });



    app.get('/list', function (req, res) {
       fs.readFile( __dirname + "/../data/" + "user.json", 'utf8', function (err, data) {
           console.log( data );
           res.end( data );
       });
    })


}

__dirname 은 현재 모듈의 위치를 나타냅니다.
router 모듈은 router 폴더에 들어있으니, data 폴더에 접근하려면
/../ 를 앞부분에 붙여서 먼저 상위폴더로 접근해야합니다.

 

자 서버를 실행해서 http://localhost/list 에 접속해보세요.

 

df

 

두번째 API:  GET /getUser/:username

이번엔 특정 유저 username의 디테일한 정보를 가져오는 GET API 를 작성해보도록 하겠습니다.

다음 코드를 router/main.js 의 list API 아래에 작성해주세요.

    app.get('/getUser/:username', function(req, res){
       fs.readFile( __dirname + "/../data/user.json", 'utf8', function (err, data) {
            var users = JSON.parse(data);
            res.json(users[req.params.username]);
       });
    });

파일을 읽은후, 유저 아이디를 찾아서 출력해줍니다.
유저를 찾으면 유저 데이터를 출력하고 유저가 없다면
{} 을 출력하게 됩니다.
fs.readFile()로 파일을 읽었을 시엔 텍스트 형태로읽어지기 때문에, JSON.parse() 롤 해야합니다.

서버를 다시 실행 후 http://localhost:3000/getUser/first_user 에 접속해보세요.

Untitled-2

세번째 API: POST addUser/:username
body: { “password”: “_____”, “name”: “_____” }

이번 API는 첫째 둘째와 다르게 POST 메소드를 사용합니다.

편한 테스팅을 위하여 구글 크롬 익스텐션 Postman 을 사용하겠습니다.unnamed_2_

본격 Postman 리뷰글

HTTP 패킷을 요청하고 분석 할 수 있는 툴 입니다. 정말 괜찮은 앱이니 받아두세요. 물론 비슷한 프로그램이 이미 설치되있는사람들은 생략하셔도 됩니다.

다음 코드를 router/main.js 의 getUser API 하단에 작성해주세요:

    app.post('/addUser/:username', function(req, res){

        var result = {  };
        var username = req.params.username;

        // CHECK REQ VALIDITY
        if(!req.body["password"] || !req.body["name"]){
            result["success"] = 0;
            result["error"] = "invalid request";
            res.json(result);
            return;
        }

        // LOAD DATA & CHECK DUPLICATION
        fs.readFile( __dirname + "/../data/user.json", 'utf8',  function(err, data){
            var users = JSON.parse(data);
            if(users[username]){
                // DUPLICATION FOUND
                result["success"] = 0;
                result["error"] = "duplicate";
                res.json(result);
                return;
            }

            // ADD TO DATA
            users[username] = req.body;

            // SAVE DATA
            fs.writeFile(__dirname + "/../data/user.json",
                         JSON.stringify(users, null, '\t'), "utf8", function(err, data){
                result = {"success": 1};
                res.json(result);
            })
        })
    });

JSON 형태가 INVALID 하다면 오류를 반환하고, VALID 하다면 파일을 열어서 username의 중복성을 확인 후
JSON 데이터에 추가하여 다시 저장합니다.

34번줄에서 stringify(users, null, 2) 은 JSON 의 pretty-print 를 위함 입니다.

 

Postman을 통하여 API를 테스트해볼까요?

br

body 에서 Content-type를 JSON 으로 하셔야 정상적으로 처리됩니다.

네번째 API: PUT updateUser/:username
body: { “password”: “_____”, “name”: “_____” }

이 API는 위 API와 비슷합니다. 사용자 정보를 업데이트 하는 API 이구요, PUT 메소드를 사용합니다.

PUT API 는 idempotent 해야 합니다, 쉽게말하자면 즉 요청을 몇번 수행하더라도, 같은 결과를 보장해야합니다.

edd

마지막 API: DELETE deleteUser/:username

유저를 데이터에서 삭제하는 API 입니다. DELETE 메소드를 사용합니다.
네번째 API 아래에 다음 코드를 작성해주세요:

    app.delete('/deleteUser/:username', function(req, res){
        var result = { };
        //LOAD DATA
        fs.readFile(__dirname + "/../data/user.json", "utf8", function(err, data){
            var users = JSON.parse(data);

            // IF NOT FOUND
            if(!users[req.params.username]){
                result["success"] = 0;
                result["error"] = "not found";
                res.json(result);
                return;
            }

            delete users[req.params.username];
            fs.writeFile(__dirname + "/../data/user.json",
                         JSON.stringify(users, null, '\t'), "utf8", function(err, data){
                result["success"] = 1;
                res.json(result);
                return;
            })
        })

    })

6262

router/main.js 전체 코드

module.exports = function(app, fs)
{

     app.get('/',function(req,res){
         res.render('index', {
             title: "MY HOMEPAGE",
             length: 5
         })
     });

    app.get('/list', function (req, res) {
       fs.readFile( __dirname + "/../data/user.json", 'utf8', function (err, data) {
           console.log( data );
           res.end( data );
       });
    });

    app.get('/getUser/:username', function(req, res){
       fs.readFile( __dirname + "/../data/user.json", 'utf8', function (err, data) {
            var users = JSON.parse(data);
            res.json(users[req.params.username]);
       });
    });

    app.post('/addUser/:username', function(req, res){

        var result = {  };
        var username = req.params.username;

        // CHECK REQ VALIDITY
        if(!req.body["password"] || !req.body["name"]){
            result["success"] = 0;
            result["error"] = "invalid request";
            res.json(result);
            return;
        }

        // LOAD DATA & CHECK DUPLICATION
        fs.readFile( __dirname + "/../data/user.json", 'utf8',  function(err, data){
            var users = JSON.parse(data);
            if(users[username]){
                // DUPLICATION FOUND
                result["success"] = 0;
                result["error"] = "duplicate";
                res.json(result);
                return;
            }

            // ADD TO DATA
            users[username] = req.body;

            // SAVE DATA
            fs.writeFile(__dirname + "/../data/user.json",
                         JSON.stringify(users, null, '\t'), "utf8", function(err, data){
                result = {"success": 1};
                res.json(result);
            })
        })
    });


    app.put('/updateUser/:username', function(req, res){

        var result = {  };
        var username = req.params.username;

        // CHECK REQ VALIDITY
        if(!req.body["password"] || !req.body["name"]){
            result["success"] = 0;
            result["error"] = "invalid request";
            res.json(result);
            return;
        }

        // LOAD DATA
        fs.readFile( __dirname + "/../data/user.json", 'utf8',  function(err, data){
            var users = JSON.parse(data);
            // ADD/MODIFY DATA
            users[username] = req.body;

            // SAVE DATA
            fs.writeFile(__dirname + "/../data/user.json",
                         JSON.stringify(users, null, '\t'), "utf8", function(err, data){
                result = {"success": 1};
                res.json(result);
            })
        })
    });


    app.delete('/deleteUser/:username', function(req, res){
        var result = { };
        //LOAD DATA
        fs.readFile(__dirname + "/../data/user.json", "utf8", function(err, data){
            var users = JSON.parse(data);

            // IF NOT FOUND
            if(!users[req.params.username]){
                result["success"] = 0;
                result["error"] = "not found";
                res.json(result);
                return;
            }

            // DELETE FROM DATA
            delete users[req.params.username];

            // SAVE FILE
            fs.writeFile(__dirname + "/../data/user.json",
                         JSON.stringify(users, null, '\t'), "utf8", function(err, data){
                result["success"] = 1;
                res.json(result);
                return;
            })
        })

    })

}

이렇게 Express 응용 RESTful API 편을 마치도록 하겠습니다.
다음 편에서는 세션을 다루도록 하겠습니다.

list

  • crmk35

    두번째 getUser/:username을 테스트하는데
    for문에서 users에 인덱스로 접근하면 계속 undefined 가 뜨네용..혹시나해서
    users[req.params.username]으로 하면 값을 찾긴 합니다만 뭐가 문제인거죵 ㅠ

    • 뭐가 문제냐하면.. 제 문제입니다.. 🙁
      왠지는 모르겠는데 제가 강좌를 작성하면서 실수를 한 것 같네요!

      최종코드에있는

      app.get(‘/getUser/:username’, function(req, res){
      fs.readFile( __dirname + “/../data/user.json”, ‘utf8’, function (err, data) {
      var users = JSON.parse(data);
      res.json(users[req.params.username]);
      });
      });

      이대로 하는게 맞습니다. .
      루프 방식으로 찾는다면

      data[Object.keys(data)[0]] 이나
      for (o in data) { … } 방식으로 찾는게 맞구요.

      강좌 수정하였습니다. 감사합니다 ㅎㅎ

  • 웅이아버지

    nodejs 하면 할 수록 막강하다는걸 느끼네요 HTTP 메소드 잘 배우고 갑니다.
    Post맨을 통해서 인서트 업데이트 딜리트 조회 까지 다 할 수 있으니 넘 좋네여

  • Pyunghwa Seo

    강좌 따라가면서 계속 실습 하고 있는데 하나 궁금한 게 있네요..
    app.get(‘/’,function(req,res){
    res.render(‘index’, {
    title: “MY HOMEPAGE”,
    length: 5
    })
    });

    app.get(‘/list’, function (req, res) {
    fs.readFile( __dirname + “/../data/” + “user.json”, ‘utf8’, function (err, data) {
    console.log( data );
    res.end( data );
    });
    })

    app.get(‘/getUser/:username’, function(req, res){
    fs.readFile( __dirname + “/../data/user.json”, ‘utf8’, function (err, data) {
    var users = JSON.parse(data);
    res.json(users[req.params.username]);
    });
    });

    세 개 코드 보면 각각 마지막에 세미콜론(;) 이게 있을 때도 있고 없을 때도 있는데
    비슷한 코드에서도 다르게 사용하셔서 어떤 의미가 있는건가여? 아니면 크게 상관 없는 건가요?

    • JavaScript에선 semicolon을 써도되고 안써도 됩니다.

      제가 이 강좌 작성할때쯤에는 jshint나 esling같은 linting도구를 사용하지 않아서..

      왠만하면 그런 문법검사도구 사용해서 세미콜런을 사용할거면 다 사용하고 사용하지않을거면 아예 세미콜론을 안쓰는게 좋습니다.

      • Pyunghwa Seo

        그렇군요..
        한가지 더 궁금한게 있는데..
        세미콜론하고 마찬가지로 똑같은 코드인데 따옴표(‘) 쓰시는 분이 있고, 쌍따옴표(“) 쓰시는 분이 있는데
        어떤 걸 쓰든지 상관이 없나요?

        • 우선 JSON 객체를 작성 할 때는 ” 를 사용하는것이 맞구요
          그 외에는 결국 이것도 스타일과 컨벤션의 차이입니다.

          오늘 검색해보니 Single Quote 가 더 많이 사용된다고 하네요

  • 다니엘

    감사합니다

  • 포스트잇

    소스에서 app.get은 되지만 app.post, app.delete, appupdate는 작동하지 않습니다.
    Cannot GET *** 출력됩니다.

    그래서 아래와 같이 바꿨습니다.

    app.get(‘/list’, function (req, res) {
    res.end(‘It worked!’);
    });

    // /list는 접속이 됩니다. it worked 출력됩니다.

    app.post(‘/addUser’, function(req, res){

    res.end(‘It worked!’);
    });

    // /adduser은 접속이 되지 않습니다. Cannot GET /addUser 출력됩니다.
    원인이 무엇인가요?

    • 삽지리

      get post delete update 는 다 http method입니다.
      https://ko.wikipedia.org/wiki/HTTP

      제대로 테스트 하려면 postman을 사용해서 url 왼쪽을 클릭하면 나오는 부분을 get post delete update 로 바꿔야 합니다.

      • 포스트잇

        답변 감사합니다.

  • 포스트잇

    조건문에 문제가 있어보입니다. add.put 라우터에서 //check req validity 영역의 코드요.
    // CHECK REQ VALIDITY
    if(!req.body[“password”] || !req.body[“name”]){
    result[“success”] = 0;
    result[“error”] = “invalid request”;
    res.json(result);
    return;
    }

    이 부분을 if(req.body[“password”] || req.body[“name”]){
    ….}
    이렇게 해줘야 add.put이 실제로 작동할 것 같습니다. 이름과 패스워드가 데이터에 있다만 에러 메시지를 내는 말이니까요.
    다시말해 서 if(!req.body[“password”] || !req.body[“name”]){…}는 패스워드가 같지 않거나 이름이 같지 않으면 true로 {}안의 error를 내라는 말인데요. 이러면 새로운 데이터를 put으로 넣을 수 없으니까요.

    • 포스트잇

      수정합니다. add put가 아니라 add post가 그렇습니다. add post이걸 뭐라 불러야할지 모르겠네요. 라우터는 아닌 것 같고요. 아무튼 add put, add post 둘다 // CHECK REQ VALIDITY
      if(!req.body[“password”] || !req.body[“name”]){

      …} 로 했으므로 새 데이터를 넣거나 업데이트할 때 무조건 “invalid request”를 내겠으니 문제입니다. !를 때야 될 것 같습니다.
      그리고 DELLET부분은 username만 알면 아무나 지울 수 있는 코드 같습니다.
      세션과 연동하면 좋겠습니다.
      다음 강좌인 세션영역에 배우는
      express-session 모듈에서 ,
      var sess = req.session;
      username: sess.username 이 코드를 응용하여 여기에 스파게티 코드를 만들면요. 특정 유저만 PUT, UPDATE, DELETE를 사용할 수 있을 것 같기도 합니다.

  • GT M

    왜 하나하나 다 설명을 안해주고 대충 넘어가는 거죠 ?

    • 하나 하나 짚고 넘어갔습니다
      만약에 이해가 안되는게 많다면 사전지식이 좀 부족한것입니다.
      설명이 더 필요하면 책을 사서 보세요. 더 검색을 해보셔도 되구요.

      • GT M

        이해가 안되는게 많진 않습니다만.. 설명이 꽤나 짧아서요

    • Johnny Koo

      위 내용은 생각보다 굉장히 자세히 쓰여져 있는 것 같은데요. 작성자님께서 얼마나 많은 시간을 들여야 이런 강좌 글을 쓸 수 있는지 잘 모르시는 것 같네요. 본인께서 한 번 node.js / express 로 hello world 찍는 것 강좌 한 번 만들어 보시면 알 것 같은데요.
      위 내용보다 더 자세히 강좌가 쓰여지긴 어려울 것 같네요

  • js공부중

    addUser 할때 안됐었는데 밑에 포스트잇님 말처럼
    // CHECK REQ VALIDITY
    if(!req.body[“password”] || !req.body[“name”]){
    부분을

    if(req.body[“password”] || req.body[“name”]){
    으로 바꾸니까 정상동작 하네요~!

    • // CHECK REQ VALIDITY
      부분이 비밀번호나 이름이 제대로 되어있는지 체크를 하고 제대로 안되어있으면 작업을 중지하는건데
      만약에 위처럼하면 password 가 null / undefined 가 아니라면 작업이 중지될텐데… 코드에 뭔가 잘못된게 있지 않을까요?

  • js공부중

    그리고 작성자님은 updateUser 실행시 password와 name 값이 나타나는데 저는 ‘first_user’값으로 put 실행하면 password와 name 값이 사라집니다.
    user.json에서 아래처럼 변경되요!
    “first_user”: {},

    왜그런건가요??

    • 제가 보기엔 req.body 가 제대로 전달되지 않은 것 같아요.

  • jk

    TypeError: Cannot read property ‘readFile’ of undefined
    at D:npmexpressroutermain.js:12:10

    –> 이런식으로 에러가 나는데 왜 그렇죠? fs.를 접근 못하는듯 보입니다.

  • 진현수

    좋은 포스팅 감사합니다. ^^
    node.js 를 처음 공부하는 입장으로 궁금한점이 있는데 질문하나만 드려도 될까요?
    본문 내용중에 node.js가 DBMS로 NoSQL 인 mongoDB에 잘 어울린다는 내용이 있었는데 어떤점이 어떻게 어울린다는 것인지 궁금합니다. node.js 가 특정 db인 mongoDB말고 전체적으로 NoSQL과 잘 어울리는 것 인가요? (NoSQL은 다루어 본 적이 없고 RDBMS만 사용해봤습니다.) NoSQL이나 관련 포스팅 정보도 있을까요?