Help us understand the problem. What is going on with this article?

サルでも分かるExpressでのjsonAPIサーバーの作り方

サル並みの語彙力の僕が書いた記事です。
istock_000018606811xsmall.jpg

事前知識

APIサーバーとは

Webサービスのバックエンドによく使われる。
「PHP入門」などでWebサービスを作るシーンが多く見られますがその違いは簡単に言うと「Webサービスの外部から利用できるかできないか」です。

多くのPHP入門の記事ではPHPでMySQLなどのデータベースからデータを取ってきてそれをそのままechoなどで表示する例がよく見られます。
しかし最近のWebサービス開発ではフロントエンドの役割をする部分がバックエンドのAPIサーバーにリクエストを投げてAPIサーバーがデータベースからデータを取ってきてそれをフロントエンドに返しページに表示するのが多い気がします。

APIサーバーを立てるメリットとして僕が一番思うのはそのアプリのスマホ版を作る時に楽です。

主なAPIサーバーの機能をまとめると「 受け取ったリクエストに対してそれに応じたデータベース操作をして結果を返してあげる 」です。

jsonAPIサーバーとは

結果をjson形式で返すAPIサーバーです。
他にはXMLで返すapiサーバーもあります。

環境

バージョンは一桁目以外は気にしないで大丈夫です。
Expressは後に説明するので他の4つを揃えといてください。

Node.js

v5.7.1
サーバーサイドでJavaScript動かすやつ

Express

4.13.1
Node.jsの上で動くMVCフレームワーク
jsonAPIサーバーとしての役割以外にejsというテンプレートエンジンを用いたWeb開発もできます

MongoDB

3.2.3
ドキュメント指向のデータベース

npm

3.10.7
Node.jsのライブラリやパッケージを管理するパッケージマネージャ

postman

httpリクエストを送ってAPIのテストを行うことができる

データベースの構成

今回はWebサービスによくある「投稿記事(article)」と「ユーザー(user)」のデータを扱うのでデータベースは以下のようです

Node.jsでMongoDBを利用して読み書きを行う際、この一つのデータの塊をモデルとして扱います

投稿記事(article)

カラム名 説明
title String 記事タイトル
text String 記事の本文
date String 投稿日時
_id ObjectId ※ 投稿記事固有のID

ユーザー(user)

カラム名 説明
name String ユーザーの名前
screen_name String Twitterの @~~ みたいな
bio String 軽い自己紹介
_id ObjectId ※ ユーザー固有のID

※ ObjectIdとは = mongodbが他と被らないように振ってくれるIDです

練習用に作ってみるAPI

method url 説明
GET /api/v1/article 記事一覧を取得
GET /api/v1/article/:id :idで指定したの記事を取得
POST /api/v1/article 記事投稿
DELETE /api/v1/article/:id :idで指定した記事を削除
GET /api/v1/user:/ ユーザー一覧を取得
GET /api/v1/user:/id :idで指定したユーザー情報取得
POST /api/v1/user/ ユーザーの新規追加
PUT /api/v1/user/:id :idで指定したユーザー情報更新

ちょっと多いですが頑張りましょう

準備

npm init

$ mkdir express-api
$ cd express-api
$ npm init

今回はexpress-apiをプロジェクトのディレクトリとします。
作ったディレクトリの中でnpm initを実行してください。いくつか質問が出てきますが全て空欄でエンターを押して大丈夫です。

ディレクトリ構成

とりあえずこういう感じで作ってください

express-api
 |---- /app
 |   └ app.js
 |   └ /models
 |ㅤ   └ /route
 |     └ v1
 |---- package.json

app.js これを実行してサーバーを起動します
/route このディレクトリは各apiのコードを書いたjsファイル入れていきます
/models データベースのモデル情報を書いたjsファイルを入れていきます
package.js npm initによって生産されたものです

必要なライブラリなどのインストール

$ npm install express --save
$ npm install mongodb --save
$ npm install mongoose --save
$ npm install body-parser --save
$ npm install moment --save

npmを使ってインストールしていきます。
末尾に--saveをつけるとpackage.jsonに書き込んでくれます。
package.jsonに書き込むと他人のPCにこのプロジェクトを送った時にnpm installと打つだけでpackage.jsonに記載されたものが全てインストールされます。便利ですね!!

コードを書く

Hello,World

まずは先ほど書いたapiではなくテスト用に以下のようなapiを作ります

method url 説明
GET /api/v1/ 「Hello,world」とjsonが返ってきます
app.js
// ライブラリ読み込み
var express    = require('express');
var app        = express();
var bodyParser = require('body-parser');

//body-parserの設定
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

var port = process.env.PORT || 3000; // port番号を指定


// GET http://localhost:3000/api/v1/
app.get('/api/v1/',function(req,res){
    res.json({
        message:"Hello,world"
    });
});

//サーバ起動
app.listen(port);
console.log('listen on port ' + port);

実行してみましょう。

$ node app/app.js
listen on port 3000

サーバーはこれでポート番号3000に起動できました。
res.json()で結果を返しています。
postmanを使ってhttp://localhost:3000/api/v1/にGETリクエストを送りましょう(postmanの使い方は省略します)
以下のように返ってくれば成功です!!

{
  "message": "Hello,world"
}

apiをディレクトリで分けて綺麗にする

まず、app.jsを以下のように書き換えます

app.js
var express    = require('express');
var app        = express();
var bodyParser = require('body-parser');

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

var port = process.env.PORT || 3000;

var router = require('./routes/v1/');
app.use('/api/v1/', router);

//サーバ起動
app.listen(port);
console.log('listen on port ' + port);

新しく追加した二行で「/routes/v1/index.jsをrouterとして読み込むよー」という処理と「それを/api/v1/のapiとして使うよー」という処理をしてます。
app.jsの既存のapp.get()部分は消してください。

ちなみに、/routes/v1//routes/v1/index.jsとして扱われます。ディレクトリを参照した場合、その下のindex.jsが読み込まれます。

次に、apiの処理をapp/routes/v1index.jsを作りましょう

index.js
var express = require('express');
// ルーティングするで
var router = express.Router();

// routerにルーティングの動作を書いてく
router.get('/',function(req,res){
    res.json({
        message:"Hello,world"
    });
});

//routerをモジュールとして扱う準備
module.exports = router;

このコードをapp.jsから呼び出すのでrouter.getの第一引数は / で大丈夫です。app.jsで「それを/api/v1/のapiとして使うよー」と宣言してるのでこのapiはhttp://localhost:3000/api/v1/として使えます。

サーバーを再起動してpostmanでapiを叩いても結果は先ほどと変わらずしっかり動くはずです

さらに分ける

ここからついに本題です(`・ω・´)
article関係のapiとuser関係のapiをどっちもindex.jsに書くものアリです。
しかしそれだとapiが増えた時読みづらいのでroutes/v1/の中にarticle.jsuser.jsを作りapiを分けましょう。
試しに各種test用のapiを作ります

method url 説明
GET /api/v1/article/test 「This is article api」と返す
GET /api/v1/user/test 「This is user api」と返す

そうなるとディレクトリ構成は以下のような感じです

express-api
 |---- /app
 |   └ app.js
 |   └ /models
 |ㅤ   └ /route
 |     └ v1
 |      └ index.js
 |      └ article.js
 |      └ user.js
 |---- package.json

それではindex.jsを以下のように書き換えましょう

index.js
var express = require('express');
var router = express.Router();

router.use('/article', require('./article.js'));
router.use('/user', require('./user.js'));

module.exports = router;

新たに追加された2行は「article.jsの中に書いてあるapiを/articleとして使うよー」と「user.jsの中に書いてあるapiを/userとして使うよー」という意味です。
つまりindex.jsから見れば「それを/api/v1/のapiとして使うよー」と言ってるので/articleとして扱うということはapp.jsから見た時/api/v1/article/として扱われることを意味します。

それでは中身を書いていきます

article.js
var express = require('express');
var router = express.Router();

// GET  http://localhost:3000/api/v1/article/test
router.get('/test', function (req, res) {
    res.json({
        message:"This is article api"
    });
});

//routerをモジュールとして扱う準備
module.exports = router;
user.js
var express = require('express');
var router = express.Router();

// GET  http://localhost:3000/api/v1/user/test
router.get('/test', function (req, res) {
    res.json({
        message:"This is user api"
    });
});

//routerをモジュールとして扱う準備
module.exports = router;

サーバーを再起動してpostmanでapiを叩いてみましょう
http://localhost:3000/api/v1/article/testにリクエストを送った時

{
  "message": "This is article api"
}

と結果が返ってくると思います。
http://localhost:3000/api/v1/user/testも上手く結果が返ってくればバッチリです!!この2つのapiはテスト用なので消してしまっても構いません。

DB操作を含むapiを作っていく

DB起動

ターミナルをもう1つ開き以下のように実行してください。

$ sudo mongod --dbpath /var/db/mongo/

これでMongoDBが起動し、読み書きができる状態になりました

Modelファイル作成

データベースの準備から始めます

データベースに入れるデータを書いたファイルを/modelsに作ります
それぞれarticleModel.jsuserModel.jsとしましょう

articleModel.js
var mongoose     = require('mongoose'); //mongoDBに接続するためのライブラリ
var Schema       = mongoose.Schema; //mongoDBのスキーマを作る
var moment       = require('moment');

var ArticleSchema   = new Schema({
    title :String,
    text: String,
    date: String
});

ArticleSchema.methods.setDate = function () { 
    //作った時間をセット
    this.date = moment().format("YYYY-MM-DD HH:mm:ss"); 
};

// スキーマをモデルとしてコンパイルし、それをモジュールとして扱えるようにする
module.exports = mongoose.model('ArticleModel', ArticleSchema);
userModel.js
var mongoose     = require('mongoose'); //mongoDBに接続するためのライブラリ
var Schema       = mongoose.Schema; //mongoDBのスキーマを作る

var UserSchema   = new Schema({
    name :String,
    screen_name: String,
    bio: String
});

// スキーマをモデルとしてコンパイルし、それをモジュールとして扱えるようにする
module.exports = mongoose.model('UserModel', UserSchema);

簡単に流れを説明すると、mongooseを読み込み、スキーマを作るのに必要なカラム名とデータ型を記述していき、最後の行でスキーマをモデルに変換してモジュールとして扱うようにしています。

ArticleModel.jsに見られるArticleSchema.methods.setDateに代入した関数はNode.jsからそれぞれの値を入れるときに呼び出すための関数です。
この場合、ArticleSchemaのdateに現在時刻をセットしてます。
また、_idは自動生産されます。

app.js編集

まずapp.jsに以下を追記しましょう(4行目で大丈夫です)

var mongoose   = require('mongoose');

mongoose.Promise = global.Promise;
mongoose.connect('mongodb://localhost:27017/ExpressAPI');
mongoose.connection.on('error', function(err) {
    console.error('MongoDB connection error: ' + err);
    process.exit(-1);
});

 

それではユーザーの新規追加APIを作りましょう
(本来ユーザー登録関係のapiにはtoken発行などが必要ですが今回は省略し、ユーザーの基本情報を新しく登録する処理のみとさせていただきます)

POST /api/v1/user/

user.jsを以下のようにします

user.js
var express   = require('express');
var router    = express.Router();
var UserModel = require('../../models/userModel.js');

router.post('/',function(req,res){

    // モデル作成.
    var User = new UserModel();

    // データを詰め込む
    User.name = req.body.name;
    User.screen_name = req.body.screen_name;
    User.bio =  req.body.bio;

    // 保存処理
    User.save(function(err) {
        if (err){
            // エラーがあった場合エラーメッセージを返す
            res.send(err);
        } else {
            // エラーがなければ「Success!!」
            res.json({ message: 'Success!!' });
        }
    });
});

//routerをモジュールとして扱う準備
module.exports = router;

POSTを受け取った時に実行したいのでrouter.post()で書きます。
コメント文の通り、userModel.jsを使い、モデルを作成し、そこにデータを詰め込みUser.save()でDBに保存しています。
req.body.~~で一緒に送られてきたデータが取得してます。
 

サーバーを再起動してpostmanでPOSTリクエストを投げてみましょう。
今回はnamescreen_namebioの値も一緒にリクエストしないといけないのでpostmanのBody欄でKeyとValueを指定してあげましょう
スクリーンショット 2016-11-17 1.56.58.png

エラーが出なければ保存できてます👍

GET /api/v1/user/

それでは保存したデータを取得するAPIを作ります
以下を追記してください。

user.js
router.get('/', function (req, res) {
    UserModel
        .find()
        .then(function (users) {
            res.json(users);
        });
});

全て取得するのは超簡単!!
UserModel.find()でUserModelが全て取得できます。
then()で受け取った時の処理を書きます。ここはres.jsonでそのまま結果を返してあげれば完了です!!

[
  {
    "_id": "582c8fc9e3841640e1140ce4",
    "bio": "Hello",
    "name": "太郎",
    "screen_name": "tarou04",
    "__v": 0
  },
  {
    "_id": "582c9144e3841640e1140ce5",
    "bio": "Hello",
    "name": "二郎",
    "screen_name": "jirou08",
    "__v": 0
  }
]

postmanでGETリクエストを送ってこんな感じで返ってくれば成功です

GET /api/v1/user/:id

次は末尾の:_idによって返す値を変えるapiです
先ほどDBに登録した二郎君にmongoDBが582c9144e3841640e1140ce5という素敵なidを振ってくれたのでこのidをリクエストすると二郎君の情報が取れる、といったapiを作りたいと思います!!

user.js
router.get('/:id', function (req, res) {

    var Userid = req.params.id;
    UserModel
        .findById(Userid,function (err,user) {
            res.json(user);
        });
});

第一引数でurlを指定する際に:idと書くとそれはreq.params.idとして受け取れます。
もちろん、:screen_nameと書けばreq.params.screen_nameです!

関数findById()は引数と_idが一致したデータを返します

それではpostmanでリクエストします
今回僕はhttp://localhost:3000/api/v1/user/582c91a1db5998416ca5fc93にGETリクエストを投げたので、以下のように返ってきました

  {
    "_id": "582c91a1db5998416ca5fc93",
    "bio": "Hello",
    "screen_name": "jirou08",
    "name": "二郎",
    "__v": 0
  }

この場合findById()の引数が582c91a1db5998416ca5fc93になったのでそれに一致したユーザーである二郎くんの情報が返ってきました

PUT /api/v1/user/:id

これはPUTなので指定ユーザーの情報を更新する、他のものに変えるapiです。

user.js
router.put('/:id',function (req, res) {

    var Userid = req.params.id;

    UserModel
        .findById(Userid, function(err, user) {
            if (err) {
                res.send(err);
            } else {

                user.name = req.body.name;
                user.screen_name = req.body.screen_name;
                user.bio = req.body.bio;

                user.save(function(err) {
                    if (err){
                        res.send(err);
                    } else {
                        res.json({ message: 'Success!' });
                    }
                });
            }
        });
});

簡単に言うとGET /api/v1/user/:idPOST /api/v1/user/が混ざったような感じです。
findById()で取得したデータをコールバック関数の中でuserとして扱いそこの中のデータを入れ替えてそのままsave()しています。
今回はfindById()で取得したデータを使ってるのでnew UserModel()は不要です

今回もnamescreen_namebioを含めてリクエストを送ってみましょう
スクリーンショット 2016-11-17 2.56.16.png
これでリクエストしたので二郎くんのbioを「Hello」から「buongiorno!!」に変わりました!!

POST /api/v1/article

article.js
var express   = require('express');
var router    = express.Router();
var ArticleModel = require('../../models/articleModel.js');

router.post('/',function(req,res){

    // モデル作成.
    var Article = new ArticleModel();

    // データを詰め込む
    Article.title = req.body.title;
    Article.text = req.body.text;
    Article.setDate();

    // 保存処理
    Article.save(function(err) {
        if (err){
            // エラーがあった場合エラーメッセージを返す
            res.send(err);
        } else {
            // エラーがなければ「Success!!」
            res.json({ message: 'Success!!' });
        }
    });
});

//routerをモジュールとして扱う準備
module.exports = router;

基本的な動きはPOST /api/v1/userと同じです。
唯一違う点としてデータを詰め込む際にsetDate()という関数を呼び出してる点です。
これはスキーマを作成するときにarticleModel.jsに書いた関数をここで呼び出しています。

GET /api/v1/article

user.js
router.get('/', function (req, res) {
    ArticleModel
        .find()
        .then(function (articles) {
            res.json(articles);
        });
});

GET /api/v1/user とほぼ同じなので説明は省略します

GET /api/v1/article/:id

article.js
router.get('/:id', function (req, res) {

    var Articleid = req.params.id;
    ArticleModel
        .findById(Articleid,function (err,article) {
            res.json(article);
        });
});

DELETE /api/v1/article/:id

article.js
router.delete('/:id',function(req,res){
    var Articleid = req.params.id;
    ArticleModel.remove({_id:Articleid})
        .then(function(){
        res.json({message:'Success!!'});
    });
});

こちらもとても簡単!
ArticleModel.remove({_id:Articleid})でArticleModelの中から_idが一致したものを削除しています

最終的なディレクトリ構成とコード

ディレクトリ

express-api
 |---- /app
 |   └ app.js
 |   └ /models
 |     └ articleModel.js
 |     └ userModel.js
 |ㅤ   └ /route
 |     └ v1
 |      └ index.js
 |      └ article.js
 |      └ user.js
 |---- package.json
 |---- /node_modules

コード

複数回の編集があったもののみです

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

mongoose.Promise = global.Promise;
mongoose.connect('mongodb://localhost:27017/ExpressAPI');
mongoose.connection.on('error', function(err) {
    console.error('MongoDB connection error: ' + err);
    process.exit(-1);
});

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


var port = process.env.PORT || 3000;


var router = require('./routes/v1/');
app.use('/api/v1/', router);

//サーバ起動
app.listen(port);
console.log('listen on port ' + port);
article.js
var express   = require('express');
var router    = express.Router();
var ArticleModel = require('../../models/articleModel.js');

router.post('/',function(req,res){

    // モデル作成.
    var Article = new ArticleModel();

    // データを詰め込む
    Article.title = req.body.title;
    Article.text = req.body.text;
    Article.setDate();

    // 保存処理
    Article.save(function(err) {
        if (err){
            // エラーがあった場合エラーメッセージを返す
            res.send(err);
        } else {
            // エラーがなければ「Success!!」
            res.json({message: 'Success!!'});
        }
    });
});

router.get('/', function (req, res) {
    ArticleModel
        .find()
        .then(function (articles) {
            res.json(articles);
        });
});

router.get('/:id', function (req, res) {

    var Articleid = req.params.id;
    ArticleModel
        .findById(Articleid,function (err,article) {
            res.json(article);
        });
});

router.delete('/:id',function(req,res){
    var Articleid = req.params.id;
    ArticleModel.remove({_id:Articleid})
        .then(function(){
            res.json({message:'Success!!'});
        });
});

//routerをモジュールとして扱う準備
module.exports = router;
user.js
var express   = require('express');
var router    = express.Router();
var UserModel = require('../../models/userModel.js');

router.get('/test', function (req, res) {
    res.json({
        message:"This is user api"
    });
});

router.post('/',function(req,res){

    // 新しいユーザのモデルを作成する.
    var User = new UserModel();

    // ユーザの各カラムの情報を取得する.
    User.name = req.body.name;
    User.screen_name = req.body.screen_name;
    User.bio =  req.body.bio;

       // ユーザ情報をセーブする.
    User.save(function(err) {
        if (err)
            res.send(err);
        res.json({ message: 'Success!' });
    });
});

router.get('/', function (req, res) {

    UserModel
        .find()
        .then(function (users) {
            res.json(users);
        });
});

router.get('/:id', function (req, res) {

    var Userid = req.params.id;
    UserModel
        .findById(Userid,function (err,user) {
            res.json(user);
        });
});

router.put('/:id',function (req, res) {

    var Userid = req.params.id;

    UserModel
        .findById(Userid, function(err, user) {
            if (err) {
                res.send(err);
            } else {

                user.name = req.body.name;
                user.screen_name = req.body.screen_name;
                user.bio = req.body.bio;

                user.save(function(err) {
                    if (err){
                        res.send(err);
                    } else {
                        res.json({ message: 'Success!' });
                    }
                });
            }
        });

});

//routerをモジュールとして扱う準備
module.exports = router;

あとがき

v1って何

version1です。アプリに新機能を追加するときはv2などのディレクトリ名を作ってapiを書いていくと良いと思います👍

最後に

誤字など誤ってる部分があったら教えてください。
また、わからない部分があれば@leafia78にメンション飛ばして頂ければ出来る範囲で答えます

utsuke-inc
埼玉県に拠点を置くソフトウェア開発会社です。Elixir, JavaScript, TypeScriptを主に使っています。
https://utsuke.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした