masalibの日記

システム開発、運用と猫の写真ブログです

Deno.landでREST API for MongoDb

mysqlREST APIの仕組みを作ったので

masalib.hatenablog.com

今度はMongoDbのREST APIを作りたいと思います。若干前の記事とかぶるところはあるのはご了承ください。

プログラム構成

├─routes.ts (ルーティング用)
├─server.ts (mainの起動プログラム)
├─denon.json (denonという監視プログラムの設定ファイル)
├─.env (DBの接続情報)
├─types.ts (型、interface)
│
└─controllers
  └─ products.ts(DBのやり方のプログラム)

わかりにくいのでgithubにアップしています

github.com

mysqlと違うのは.envファイルとdenon.jsonファイルが追加になった事です

事前作業

ローカルから接続できるmongodbを用意する。もしできない場合はsandboxで使えるmlabを使う。

masalib.hatenablog.com

環境変数の設定

vi .env

sample

mongo_host=XXX.mlab.com
mongo_user=denouser
mongo_password=password
mongo_db=deno_db
mongo_collection=denocollection 
mongo_port=nnnnn

denon install

変更時に自動的に再起動してくれるツールのインストール。環境設定とかも楽です。

deno install --allow-read --allow-run --allow-write -f --unstable https://deno.land/x/denon/denon.ts

実行

denon start

Routes

GET      /api/v1/products
GET      /api/v1/products/:id
POST     /api/v1/products
PUT      /api/v1/products/:id
DELETE   /api/v1/products/:id

ソース解説

server.ts

import {Application,Router} from "https://deno.land/x/oak/mod.ts";
import router from './routes.ts'

const port =  5000 
const app = new Application()
app.use(router.routes())
app.use(router.allowedMethods())
console.log(`Server Running on port ${port}`)

await app.listen({port })

起動用のプログラムなのでルーティング用のファイルを読んでいて適用させているだけです。

routes.ts

import {Router} from "https://deno.land/x/oak/mod.ts";
import {getProducts,getProduct, addProduct ,updateProduct ,deleteProduct} from './controllers/products.ts'

const router = new Router()
router.get('/api/v1/products', getProducts )
    .get('/api/v1/products/:id', getProduct )
    .post('/api/v1/products', addProduct )
    .put('/api/v1/products/:id', updateProduct )
    .delete('/api/v1/products/:id', deleteProduct )

export default router

ルーティング用のプログラムで振るまい(コントローラー)の設定をしています :id になっているところはgetのパラメータです。

types.ts

export interface Product{
    id: string;
    name: string;
    description: string;
    price: number;
}

型宣言してるだけです。javascriptと違い型宣言できるのはいいですね。

denon.json

{
  "$schema": "https://deno.land/x/denon/schema.json",
  "scripts": {
    "start": {
      "cmd": "deno run server.ts",
      "allow": [
        "env",
        "read",
        "write",
        "net",
        "plugin"
      ],
      "unstable": true
    }
  },
  "logger": {
    "debug": true
  },
  "watcher": {
    "interval": 350,
    "exts": ["js", "ts", "json"],
    "match": ["*.*"],
    "skip": ["*/.git/*" ,"README.md" ],
    "legacy": false
  }
}
  • 起動時の設定や監視ファイルの無視ファイルを設定しています。allowの設定とか多くて困る・・・この設定があると本当に楽~♪

products.ts

長いの分割になります。全文をみたい場合はgithubのソースを参照してください。

ライブラリーの読み込み

import { v4 } from "https://deno.land/std/uuid/mod.ts";
import { config } from "https://deno.land/x/dotenv/mod.ts";
import { MongoClient } from "https://deno.land/x/mongo@v0.7.0/mod.ts";
import { Product } from '../types.ts'
  • /std/uuid/mod.tsはuuidというユニークのIDを作ってくれる関数です。stdなので標準ライブラリーです。
  • /x/dotenv/mod.tsは環境変数をファイルから読み込むプログラムです。
  • /x/mongo@v0.7.0/mod.tsはmongodbに接続するためのプログラムです。xがついているのでサードパーティのライブラリーです。
  • /types.tsは自分がつくった型宣言用のプログラムです。

mongodbの接続

//環境変数
const ENV_PATH = '.env';//deno runしたところをカレントディレクトリになるみたい
const config_env:any = config({ path: ENV_PATH });
const user:string = config_env.mongo_user
const password_data:string = config_env.mongo_password
const host:string = config_env.mongo_host
const port_num:number = config_env.mongo_port
const db_name:string = config_env.mongo_db
//mongodb://<dbuser>:<dbpassword>@<host>:<port>/<db>
const url:string = 'mongodb://' + user + ':' + password_data + '@' + host + ':' + port_num + '/' + db_name

//mongoDB接続
console.log("mongodb connection start")
const client = new MongoClient()
client.connectWithUri(url)

//DBの設定
const db = client.database(db_name)

//コレクションの設定
const collection_name:string = config_env.mongo_collection
const datas = db.collection(collection_name)

環境変数を変数に設定してmongodbのconnectionのClientを作成しています。本番で使うならtrycatchした方がいいかも。

データ取得(全件)

const getProducts = async({ response }: { response: any }) => {
  try{
    const data = await datas.find();
    //判定が微妙な感じ・・・
    if ( data.toString() ===""){
      response.status = 404
      response.body = {
        success: false,
        msg: 'No Product found'
      }
    } else {
      response.body = {
          success: true,
          data: data
      }
    }
  } catch(error){
    response.status = 500
    response.body = {
      success: false,
      msg: 'Server Error'
    }
  }
}
  • データを取得するのでasyncとawaitの記載が必須です。
  • 「datas.find()」で全件取得しています。本番で動かすなら件数の制限や取得順の指定をするべき
  • データの取得判定は「data.toString()」で配列を文字列に変換する事でおこなっています。単純にif ( data)を記載してしまうと0件でも成功したと判定されてしまいます。色々と試してみたけど2020/06/03時点だとこれしかないみたい
  • 念のためにtrycatchをしています。

データ取得(1件)

const getProduct = async({ params ,response }: { params:{id :string },response: any }) => {
  try{
    const data = await datas.find({ "id": params.id });
    //console.log(params.id)
  
    //判定が微妙な感じ・・・
    if ( data.toString() ===""){
      response.status = 404
      response.body = {
        success: false,
        msg: 'No Product found'
      }
    } else {
      response.status = 200
      response.body = {
        success: true,
        data:data
      }
    }
  } catch (error){
    response.status = 500
    response.body = {
      success: false,
      msg: 'Server Error'
    }
  } 
}
  • getのIDのパラメータをもとに条件式にいれています。ユニークの設定をしていないと複数件とれてしまいますwww

新規作成

const addProduct = async ({ request, response }: { request: any , response: any }) => {
  const body = await request.body()

  if (!request.hasBody){
    response.status = 400
    response.body = {
      success: false,
      msg: 'No Data'
    } 
  }else {

    //バリデーション・・・省略
    const product: Product = body.value
    product.id = v4.generate()
    try{
      const insertId = await datas.insertOne(product);
      const product_data = await datas.findOne({ _id: insertId });//$oidを表示するため

      if ( product_data.toString() ===""  ){
        response.status = 400
        response.body = {
          success: false,
          msg: 'Insert false'
        } 
      } else {
        response.status = 200
        response.body = {
          success: true,
          data: product_data
        }
      }
    } catch(error){
      response.status = 500
      response.body = {
        success: false,
        msg: 'Server Error'
      } 
  
    }  
  }
}
  • バリデーションはめんどくさいのしていません
  • idはuuidでユニークのIDを作成しています
  • パラメータではなくリクエストの内容(JSON)を利用してデータをインサートしてます
  • 関数の部分が1件取得時と違います
1件取得         ↓パラメータ    ↓パラメータ
const getProduct = async({ params ,response }: { params:{id :string },response: any }) => {

新規作成         ↓リクエスト    ↓リクエスト
const addProduct = async ({ request, response }: { request: any , response: any }) => {
  • JSONのサンプル
 {
    "name": "Product1_add",
    "description": "description1_add",
    "price": 200
  }

mysqlと違いmongodbが独自に発行しているIDが付与されています。

更新

const updateProduct = async({ params, request, response }: { params: { id: string }, request: any, response: any }) => {
  //console.log("params.id:"+ params.id)

  const body = await request.body()

  if (!request.hasBody){
    response.status = 400
    response.body = {
      success: false,
      msg: 'No Data'
    } 
  }else {
    //バリデーション・・・省略
    const product: Product = body.value
    product.id = params.id
  
    try{
      const { matchedCount, modifiedCount, upsertedId } = await datas.updateOne(
        { "id": params.id },
        { $set: { 
          "name": product.name,
          "description": product.description,
          "price": product.price
          } }
      );
      console.log("upsertedId:" + upsertedId) //NULLがセット??どうやったらセットされるのか不明
      console.log("matchedCount:" + matchedCount) 
      console.log("modifiedCount:" + modifiedCount)
      const data = await datas.find({ "id": params.id }); //$oidを表示するため

      //成功時には{ matchedCount 1, modifiedCount 1 }が返ってくる。もう少し欲しいけど・・・
      if (matchedCount ===1 &&  modifiedCount ===1  ){
        response.status = 200
        response.body = {
          success: true,
          data: data
        }
      } else {
        response.status = 404
        response.body = {
          success: false,
          msg: 'No product found'
        } 
      }
    } catch(error){
      response.status = 500
      response.body = {
        success: false,
        msg: 'Server Error'
      } 
    }  
  }
}
  • パラメータとリクエストデータを使って更新しています.
  • bodyをそのまま渡す事ができず・・・分解してセットしています
  • 公式に書いてあるとおりにやっているのですが、upsertedId にIDがセットされません。NULLがセットされるので今回は使っていません
  • 更新の有無はmatchedCount: 1とmodifiedCount: 1 で判別しています。

削除

const deleteProduct = async({ params, response }: { params: { id: string }, response: any }) => {

  if (params.id ===""){
    response.status = 400
    response.body = {
      success: false,
      msg: 'No Data'
    } 
  }else {

    //バリデーション・・・省略
    try{
      const data = await datas.find({ "id": params.id });
      //console.log(params.id)
    
      //判定が微妙な感じ・・・
      if ( data.toString() ===""){
        response.status = 404
        response.body = {
          success: false,
          msg: 'No Product found'
        }
      } else{
        //const deleteCount = await datas.deleteOne({ "id": params.id }); mongodbのIDじゃないといけないのか?わからないのでmanyに変更
        const deleteCount = await datas.deleteMany({ "id": params.id });
        //console.log(deleteCount)
        //成功時には{ deleteCount 1 }が返ってくる。もう少し欲しいけど・・・
        if (deleteCount ===1 ){
          response.status = 200
          response.body = {
            success: true,
            msg: 'Product removed'
          }
        } else {
          response.status = 404
          response.body = {
            success: false,
            msg: 'No product found'
          } 
        }
      }

    } catch(error){
      response.status = 500
      response.body = {
        success: false,
        msg: 'Server Error'
      } 
    }  
  }
}
  • パラメータを使って削除しています。deleteOneだとうまくいかなかったのでdeleteManyでやっています
  • IDの有無と存在チェックをしています。

注意事項

実行されると.deno_pluginsというフォルダが作成されてそこにDLLがインストールされます。 これはmongodbのモジュールが不安定のためです。バージョンアップされると改善されるかと思います

感想

  • 更新の部分がjsonのまま渡せないのはめんどくさい。
  • 更新時にupsertedIdが何もセットされずはまった。バージョンアップしたら何か変わるかも。