0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PostgreSQLにExpressのルーティングをTypeScriptで実装するサンプル

Last updated at Posted at 2022-02-17

PostgreSQL(ポスグレ)にSequelize(シーケライズ)でデータベースを作成してExpressAPIを叩けるようにTypeScriptで実装するまでのまとめです。初心者向けの記事なので、知見のある方はご配慮お願いします。

package.json
"dependencies": {
    "@nuxt/typescript-runtime": "^latest",
    "@nuxtjs/axios": "^latest",
    "express": "^latest",
    "nuxt": "^latest",
    "pg": "^latest",
    "pg-hstore": "^latest",
    "sequelize": "^latest",
    "typescript": "^latest",
    "uuid": "^latest",
    "body-parser": "^latest",
    "multer": "^latest",
  },

パッケージは事前にインストールお願い致します。ドッカーコンポーズでnpmやデータベースを起動させます。

docker-compose.yml
version: 'latest'
services:
  app:
    image: node: latest
    command: npm run dev
    working_dir: /src/app
    volumes:
      - .:/src/app
    links:
      - database
    ports:
      - 3000:3000
    environment:
      - 
  db:
    image: postgres
  
    environment:
      - POSTGRES_PASSWORD=hogehoge
    volumes:
      - type: volume
        source: dbdata
        target: /var/lib/postgresql/data

volumes:
  dbdata:

まずはデータベースの作成です。Node.ts(サーバーサイドをTypeScriptで記述)のORM(sequelize)で作っていきます。

lib/database.ts
import {
  Sequelize,
  UUID,
  UUIDV4,
  INTEGER,
  STRING,
  BLOB,
  FLOAT,
  ENUM,
} from 'sequelize'
// シーケライズからインポートさせます。サジェストされる型をチョイスして下さい。

export const config = new Sequelize(
  'postgres://postgres:postgres@db/postgres',
  { define: { underscored: true } }
)
// underscored: true で命名規則をアンダースコアに指定。

export const HogeData = config.define(
  'hoge',
  {
// hogesテーブルを作成したいので。下はカラムになります。
    id: {
      type: UUID,
      defaultValue: UUIDV4,
      primaryKey: true,
      allowNull: false,
    },
// 乱数で生成されるUUIDv4はdefaultValueに設定。
    hoge_id: {
      type: UUID,
      allowNull: false,
    },
// サブでuuidを生成したいので作成。
    number: {
      type: INTEGER,
      allowNull: false,
    },
// 番号で並べ替えさせたいので数値型。
    name: {
      type: STRING,
    },
// 文字列で名前を入力。
    image: {
      type: BLOB,
    },
// バイナリデータを格納したい時はデータサイズ指定無しのBLOB型。
    x: {
      type: FLOAT,
    },
// 座標で使用したかったので浮動小数点型であるFLOATを使用。
    y: {
      type: FLOAT,
    },
    path: {
      type: STRING,
      allowNull: false,
    },
// 画像や動画のファイル名。
    mimetype: {
      type: STRING,
      allowNull: false,
    },
// ファイルの形式を挿入。
    hoge_status: {
      type: ENUM,
      values: ['none', 'hoge-on', 'hoge-off']
    },
// 列挙型で3つの定数をひとまとめに。
  },
  {}
)

const setup = async () => {
  await config.authenticate()
  HogeData.sync({ alter: true })
}
// 認証ハンドラーでデータを残して置くために alter: true。
setup()

ドッカーコンポーズのexecオプションでターミナルからデータベースを覗いてみます。上記の実装で下記hogesテーブルが作成されました。

docker-compose exec db psql -U postgres

※ exec(コマンド) db(サービス名) -U(ユーザー名) postgres(実行したいデータベース)
postgres=# \d hoges;
                          Table "public.hoges"
   Column    |           Type           | Collation | Nullable | Default 
-------------+--------------------------+-----------+----------+---------
 id          | uuid                     |           | not null | 
 hoge_id     | uuid                     |           | not null | 
 number      | integer                  |           | not null | 
 name        | character varying(255)   |           |          | 
 image       | bytea                    |           |          | 
 x           | double precision         |           |          | 
 y           | double precision         |           |          | 
 hoge_status | enum_hoges_hoge_status   |           |          | 
 created_at  | timestamp with time zone |           | not null | 
 updated_at  | timestamp with time zone |           | not null | 
 path        | character varying(255)   |           | not null | 
 mimetype    | character varying(255)   |           | not null | 
Indexes:
    "hoges_pkey" PRIMARY KEY, btree (id)

何がしたいのかって言われるとただ単に使い方をまとめているのだけなので皆さんは応用してお使い下さい。
APIの実装に移る前にreq.params req.body res.json( ) res.send( )などExpressのルーティングルートメソッドのおさらいです。ルーティングとはWEB上で情報資源を特定するのにURL(情報資源の位置)やURN(名前)がありますが、それらを総称しそのエンドポイントとなるURIとクライアントアプリケーションによるリクエストに対してレスポンスで答える定義のことです。

req.params.

デフォルト(初期値){ }の名前が付いているルート(URL)のパラメータ(値が変化する変数)にマップ(関連付けられた)されたプロパティ(フィールドなどのデータ)を含むオブジェクトのことです。

req.body.

デフォルトは未定義でリクエストをかけた場所のKEYキー(データを定義する定数で年齢品種になります)と(そこにセットされる変数で4歳お菓子になります)のペアを含めて取得できます。
※使用するのにbody-parsermulterのミドルウェアを取得して下さい。

res.json( )

引数のオブジェクトや配列をJSON.stringify()で文字列に直してコンテンツの種類を設定してレスポンスしてくれます。

res.send( )

多種多様なレスポンスが返せるメソッドで、JSONを返す結果はres.jsonと同じです。引数に配列やオブジェクトを渡した時text/jsonapplication/jsonというMIMEタイプをContent-Typeに設定してくれて処理してくれます。

res.download(path [, filename] [, options] [, fn])

(path [, filename] [, options] [, fn])内にあるfilename(添付ファイルとしてダウンロードされるファイル名)やmimetype(メディアタイプ)を転送してくれます。ファイルを一度ストレージのどこかに置く必要があります。

res.render( )

指定したビューファイルをブラウザに表示させます。( )内に画面の実装ファイルを書きます。

fs.statSync() fs.readFileSync()

import fs from 'fs'

Node.jsには公式モジュールでインポートしてくれば使えます。ファイルが存在するかどうか確認する為に使用します。ファイルがない場合エラーになるのでtryしてcatchする必要があります。同じくfs.readFileSync()はファイルの内容を読む為に使用します。

ルーティングの[使用例]

api/hoges/index.ts
import { Request, Response } from 'express'
import { HogeData } from '../../lib/db'

export const hogeList1 = async (_: Request, res: Response) => {
  res.json(await HogeData.findAll())
}
// データベースにあるテーブルのデータを全て取得
// HogeDataの中の全ての文字列データを取得してくれます。reqを使用しない場合はアンダーバーを使用。

export const hogeList2 = async (req: Request, res: Response) => {
  res.json(
    await HogeData.findAll({
      where: {
        hoge_id: req.params.hogeId,
      },
    })
  )
}
// データベースにあるルートIDにヒットしたデータを全て取得
// HogeDataのルートhogeIdを探して全て取得してきてくれます。

export const hogeList3 = async (req: Request, res: Response) => {
  res.json(
    await HogeData.findAll({
      where: {
        hoge_id: req.params.hogeId,
      },
      order: [['numbar', 'ASC',]],
    })
  )
}
// データベースにあるルートIDにヒットしたデータを並び順を考慮し全て取得。
// HogeDataの中のオブジェクトのhogeIdを探してnumbar(カラム)の値(フィールド)を昇順(ASC)で取得してくれます。降順にしたい場合は(DESC)。

export const hogeList4 = async (req: Request, res: Response) => {
  res.json(
    await HogeData.findAll({
      where: {
        hoge_id: req.params.hogeId,
      },
      attributes: { exclude: ['hoge'] },
      order: [['created_at', 'DESC']],
    })
  )
}
// 作成された日時を後ろからhoge要素を取り除いて名前付きルートのhogeIdを探して取得してくれます。


export const hogeGet1 = async (req: Request, res: Response) => {
  res.json(await HogeData.findByPk(req.params.id))
}
// findByPk(id)のidのデータのみを取得
// HogeDataの中のオブジェクトのid(uuid)を取得してくれます。

export const hogeGet2 = async (req: Request, res: Response) => {
  res.json(await HogeData.findByPk(req.params.hogeId))
}
// 上のものとほとんど変わらないですが、一応。
// HogeDataの中のオブジェクトのhogeIdを取得してくれます。

export const hogeGet3 = async (req: Request, res: Response) => {
  const { hogeId, hogehogeId } = req.params
  res.json(
    await HogeData.findOne({
      where: {
        id: hogehogeId,
        hoge_id: hogeId,
      },
      attributes: { exclude: ['hoge_hoge'] },
    })
  )
}
// IDで特定し一致したテーブルデータを全て取得
// attributes:(属性)で配列の中身(hoge_hoge)をexclude:(除外)してます。
// 名前付きのURLルートを二種類のIDを特定して条件を満たした最初のデータを呼び出してくれます。

export const hogeGet4 = async (req: Request, res: Response) => {
  const { hogeId, id } = req.params
  const HogeImage = (await HogeData.findOne({
    where: {
      id,
      hoge_id: hogeId,
    },
  })) as any
  res.download('uploads/' + HogeImage.path, HogeImage.name)
}
// IDで特定した条件を満たすテーブルの画像データを取得
// 型でエラーが出ないようにas anyでIDを型アサーションしてコンパイルエラーを防ぎます。
// HogeDataからidとhogeIdをreq.params内から検索。
// ルート下にuploadsフォルダを作って一度保存し、HogeImage.path(filename)と
// HogeImage.name(originalname)ファイルを転送。
// 割愛しますが、hogeGet5としてHogeMovie.pathの部分だけ変更すれば同様に使えます。

export const hogeCreate1 = async (req: Request, res: Response) =>
  res.json(await HogeData.create(req.body))
// クライアント側で入力されたデータをデータベースに保存
// req.bodyに作成したそれぞれのパラメータをHogeDataに保存してくれます。

export const hogeCreate2 = async (req: Request, res: Response) => {
  const hoge = req.body
  hoge.hoge_id = req.params.HogeId
  hoge.image = fs.readFileSync((req as any).file.path)
  res.json(await HogeData.create(hoge))
}
// 画像のバイナリデータをデータベースに保管するルーティング
// req.bodyをHogeDataのhogeとして、req.paramsのstring(文字列)であるIDをhoge_idに。
// 型付けをしたreq.file.pathからファイルを読み込みimageフィールドに格納。

export const hogeCreate3 = async (req: Request, res: Response) => {
  const { originalname, filename, mimetype } = (req as any).file
  const hogeMovie = Object.assign({}, req.body, {
    hoge_id: req.params.hogeId,
    name: originalname,
    path: filename,
    mimetype,
  })
  res.json(await HogeData.create(hogeMovie))
}
// 画像・動画用にデータベースに保管する為のルーティング
// vueの場合、オブジェクトに複数のプロパティを割り当てる場合Object.assign()を使用します。
// 動画をデータベースに保存するには、multerモジュールをインストールします。
// リクエスト情報であるreq.fileの中身を{ originalname, filename, mimetype }に代入し。
// データベースのhoge_idにreq.paramsからhogeIdを持ってきて。
// nameにorijinalname、pathにはfilename、mimetypeが入った配列をreq.bodyと合わせてオブジェクト化し。
// HogeDataテーブルに作成した内容を保存。

export const hogeImage = async (req: Request, res: Response) =>
  res.send(((await HogeData.findByPk(req.params.hogeId)) as any).image)
// データベースのHogeDataテーブルの特定されたIDを持つ、
// imageフィールドからデータを取得。

export const hogeUpdate = async (req: Request, res: Response) => {
  const { hogeId, id } = req.params
  const hoge = (await HogeData.findOne({
    where: {
      id,
      hoge_id: hogeId,
    }
  })) as any
  res.json(await HogeData.update({ name: req.body.name }))
}
// ルート上の2つの特定のIDをを持つテーブルのnameフィールドを更新
// name以外にも変更可能

export const hogeUpdateStatus = async (req: Request, res: Response) => {
  const { status } = req.body
  console.log({ status, h: req.body })
  res.json(await HogeData.update({
    hoge_status: status,
  }, {
    where: {
      id: req.params.hogeId
    }
  }))
}
// on/offなどのパラメータを渡すスイッチなどに使用
// ルート上の特定のIDに合致したstatusデータを更新させる
// hoge_statusにはクライアント側で入力された列挙型のデータが格納される。
// 一応、データが入ってきているかログで確認。

export const hogeDestroy1 = async (req: Request, res: Response) => {
  const { hogeId } = req.params
  res.json(
    await HogeData.destroy({
      where: {
        id: hogeId,
      },
    })
  )
}
// ルートの特定のIDを持ったテーブルのデータを削除

export const hogeDestroy2 = async (req: Request, res: Response) => {
  const { hogeId, id } = req.params
  res.json(
    await HogeData.destroy({
      where: {
        id,
        hoge_id: hogeId,
      },
    })
  )
}
// ルート上の2つの特定のIDをを持つテーブルデータを削除

axiosを導入してAPIを利用するのに呼ぶ為の場所が下記のapi/index.tsになります。

api/index.ts
import express from 'express'
import * as hoges from './hoges'
// expressとhoges/index.tsからインポート

const handler = express()
handler.use(express.json())

handler.use((req: any, res: any, next: any) => {
  res.header('Access-Control-Allow-Origin', '*')
// フロントエンドのURL
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
// 他にもPATCHやOPTIONSも許可できる「
  res.header(
    'Access-Control-Allow-Headers',
    'Content-Type, Authorization, access_token'
  )
// アプリケーションに必要なヘッダを設定

// 上記のレスポンスヘッダーをオリジンからのコードにリソースへの
// アクセスを許可するように指示するようにする

  if (req.method === 'OPTIONS') {
    res.send(200)
  } else {
    next()
  }
})

const multer = require('multer')
const upload = multer({
  dest: 'uploads/',
  limits: { fieldSize: 100 * 1024 * 1024 },
})
// multerはExpressのミドルウェアでファイルのアップロードに必要

// axiosで呼ぶ為のhogesのコード(例)
// GET,PUT,POST,DELETEをルーティングで結ぶ
handler.get('/hoges', hoges.hogeList1)

handler.post('/hoges', hoges.hogeCreate1)

handler.get('/hoges/:hogeId', hoges.hogeGet1)

handler.delete('/hoges/:hogeId', hoges.hogeDestroy1)

handler.put('/hoges/:hogeId/hogeStatus', hoges.hogeUpdateStatus)

handler.post('/hoges/:hogeId',upload.single('image'), hoges.hogeCreate2)

handler.get('/hoges/:hogeId/image', hoges.hogeImage)

handler.get('/hoges/:hogeId/hogeImages', hoges.hogeList2)

handler.get('/hoges/:hogeId', hoges.hogeGet3)

handler.post('/hoges/:hogeId/hogeImages',  upload.single('hogesImage'), hoges.hogeCreate3)

handler.get('/hoges/:hogeId/hogeMovies', hoges.hogeList4)

handler.get('/hoges/:hogeId/hogeMovies/:hogehogeId', hoges.hogeGet3)

handler.post('/hoges/:hogeId/movies', upload.single('movie'), hoges.hogeCreate3)

handler.get('/hoges/:hogeId/movies/:id', hoges.hogeCreate5)
// * 上記でも書きましたがhogeCreate5は割愛してます。
handler.delete('/hoges/:hogeId/movies/:id', hoges.hogeDestroy2)

handler.put('/hoges/:hogeId', hoges.hogeUpdate)

module.exports = {
  path: '/api',
  handler,
}

現在、クライアント側はVueで実装してますがそちらでの使い方も書いてみたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?