masalibの日記

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

Deno.landでREST API for mysql

参考にしているyoutubeがRESTAPIを作っていたのでそちらを参考にMYSQLのRESTAPIを作りたいと思います。 RESTの処理の部分はかなり参考(ぱくり)にしている

プログラム構成

├─routes.ts (ルーティング用)
├─simpleServer.ts (mainの起動プログラム)
├─types.ts (型、interface)
│
└─controllers
  └─ products.ts(DBのやり方のプログラム)

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

github.com

事前作業

mysql table 作成

CREATE TABLE `Product` (
  `id` varchar(64) NOT NULL,
  `name` varchar(256) DEFAULT NULL,
  `description` varchar(1024) DEFAULT NULL,
  `price` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

環境変数の設定

windows

set MYSQL_HOST_ENV=XXX.XXX.XXX.XXX
set MYSQL_USERNAME_ENV=XXXXXXXXXX
set MYSQL_DB_ENV=XXXXXXXXXX
set MYSQL_PASSWORD_ENV=XXXXXXXXXX
set MYSQL_PORT_ENV=3306

Linux

export MYSQL_HOST_ENV=XXX.XXX.XXX.XXX
export MYSQL_USERNAME_ENV=XXXXXXXXXX
export MYSQL_DB_ENV=XXXXXXXXXX
export MYSQL_PASSWORD_ENV=XXXXXXXXXX
export MYSQL_PORT_ENV=3306

実行

deno run --allow-net --allow-env server.ts

Routes

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

ソース解説

simpleServer.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と違い型宣言できるのはいいですね。

products.ts

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

ライブラリーの読み込み

import { v4 } from "https://deno.land/std/uuid/mod.ts";
import { Client } from "https://deno.land/x/mysql/mod.ts";
import { Product } from '../types.ts'

/std/uuid/mod.tsはuuidというユニークのIDを作ってくれる関数です。stdなので標準ライブラリーです。
/x/mysql/mod.tsはmysqlに接続するためのプログラムです。xがついているのでサードパーティのライブラリーです。
/types.tsは自分がつくった型宣言用のプログラムです。

mysqlの接続

//環境変数
const hostname = Deno.env.get("MYSQL_HOST_ENV") as string
const username = Deno.env.get("MYSQL_USERNAME_ENV") as string
const db = Deno.env.get("MYSQL_DB_ENV") as string
const password = Deno.env.get("MYSQL_PASSWORD_ENV") as string
const port = parseInt(Deno.env.get("MYSQL_PORT_ENV") as string) //環境変数はstringなので string to numberにする

//mysqlに接続する
const client = await new Client().connect({
  hostname: hostname,
  username: username,
  db: db,
  password: password,
  port:port
});

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

データ取得(全件)

const getProducts = async({ response }: { response: any }) => {
  try{
    const data = await client.query(`SELECT id , name , description, price  FROM Product;`)
    //判定が微妙な感じ・・・
    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の記載が必須です。
  • データの取得判定は「data.toString()」で配列を文字列に変換する事でおこなっています。単純にif ( data)を記載してしまうと0件でも成功したと判定されてしまいます。色々と試してみたけど2020/06/03時点だとこれしかないみたい
  • 念のためにtrycatchをしています。
  • 取得後にconnectionを開放する処理はいれていません。いれると同時アクセスで落ちる事がありました。

データ取得(1件)

const getProduct = async({ params ,response }: { params:{id :string },response: any }) => {
  try{
    const data = await client.query(
      "select id,name,description,price from Product where id = ? ",
      [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のパラメータをもとにSQLを作っています。SQLインジェクションはできていました(16進数では試していません)

新規作成

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{
      let result = await client.execute(`
      INSERT INTO Product
        (id,name, description, price)
        VALUES
        (?,?,?,?);
        `, [
          product.id,
          product.name,
          product.description,
          product.price
      ]);
      //成功時には{ affectedRows: 1, lastInsertId: 0 }が返ってくる。もう少し欲しいけど・・・
      if (result.affectedRows ===1 ){
        response.status = 200
        response.body = {
          success: true,
          data: product
        }
      } else {
        response.status = 400
        response.body = {
          success: false,
          msg: 'Insert false'
        } 
      }

    } 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
  }

更新

const updateProduct = async({ params, request, response }: { params: { id: string }, 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 = params.id

    try{
      let result = await client.execute(`
      UPDATE Product
      SET
      name = ?,
      description = ?,
      price = ?
      WHERE id = ?;
      `, [
          product.name,
          product.description,
          product.price,
          product.id,
      ]);

      //成功時には{ affectedRows: 1, lastInsertId: 0 }が返ってくる。もう少し欲しいけど・・・
      if (result.affectedRows ===1 ){
        response.status = 200
        response.body = {
          success: true,
          data: product
        }
      } else {
        response.status = 404
        response.body = {
          success: false,
          msg: 'No product found'
        } 
      }

    } catch(error){
      response.status = 500
      response.body = {
        success: false,
        msg: 'Server Error'
      } 
    }  
  }
}
  • パラメータとリクエストデータを使って更新しています.
  • 更新の有無はaffectedRows: 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{
      let result = await client.execute(`
      DELETE FROM Product
      WHERE id = ?;
      `, [
        params.id,
      ]);

      //成功時には{ affectedRows: 1, lastInsertId: 0 }が返ってくる。もう少し欲しいけど・・・
      if (result.affectedRows ===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'
      } 
    }  
  }
}
  • パラメータを使ってDELETEのSQLを作っています。
  • IDの有無ぐらいしかチェックしていません

感想

  • ベースとなる部分ができたのでうれしい。
  • 他のDBの部分もClientの部分を変えればいけると思うので挑戦したい
  • ORラッパーもあるみたいなので次はそちらで作りたい

参考URL

youtu.be

github.com