MySQL | SQLite経験者向け、30日で習得するMySQL:実務応用 - Day25 API連携

SQL MySQL
スポンサーリンク

Day25 後半のゴール

「“API ⇔ DB”をコードでつなぎ、HTTPの振る舞いまで意識できるようになる」

前半で、「ブラウザ → API → DB」の流れと、GET/POST の役割はイメージできました。
後半では、それを実際のコード(Node.js と Python のどちらか)に落として、「APIがどうDBを触るか」を具体的に見ていきます。

ここでのゴールはこうです。

HTTPステータスコード(200 / 201 / 400 / 404 / 500)の意味をざっくり理解する
GET(取得)と POST(登録)のAPIを、DBアクセス込みでコードとして読める
バリデーションとエラーハンドリングを「ないと危ないもの」として認識できる

Node.js と Python の両方を出しますが、「どっちか一つをちゃんと追う」でOKです。


HTTPステータスコードをざっくり押さえる

「“成功か失敗か・どんな失敗か”を数字で伝える」

APIは、JSONだけでなく「ステータスコード」でも結果を伝えます。
最低限、次の5つだけ覚えておけば十分です。

200 OK
データ取得などが正常に成功したとき

201 Created
新しいリソース(ユーザーなど)が作られたとき

400 Bad Request
リクエストがおかしい(バリデーションエラーなど)

404 Not Found
指定されたリソースが存在しない

500 Internal Server Error
サーバー側で予期しないエラーが起きた

「成功したのに 200 を返さない」「バリデーションエラーなのに 500 を返す」
こういうAPIは、使う側からするととてもつらいです。

なので、

ビジネス的に正しくない入力 → 400
サーバーのバグやDB障害 → 500

という線引きを、最初から意識しておくと良いです。


Node.js(Express)+ MySQL でのAPI例

「GET /api/users と POST /api/users を最後まで追ってみる」

まずは Node.js 版から。
前提として、Day24 で作った mysql2/promise の接続プールがあるとします。

// db.js
const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: process.env.DB_HOST || 'localhost',
  user: process.env.DB_USER || 'sample_user',
  password: process.env.DB_PASSWORD || 'secret_password',
  database: process.env.DB_NAME || 'sample_app',
  waitForConnections: true,
  connectionLimit: 10,
});

module.exports = { pool };
JavaScript

GET /api/users(ユーザー一覧取得)

// app.js
const express = require('express');
const { pool } = require('./db');

const app = express();
app.use(express.json());

app.get('/api/users', async (req, res) => {
  try {
    const [rows] = await pool.execute(
      'SELECT id, name, email FROM users ORDER BY id ASC'
    );
    res.status(200).json(rows);
  } catch (err) {
    console.error(err);
    res.status(500).json({ message: 'Internal server error' });
  }
});

app.listen(3000, () => {
  console.log('API server listening on http://localhost:3000');
});
JavaScript

ここでやっていることを分解すると、

GET /api/users に来たら、この関数が呼ばれる
DBから SELECT id, name, email FROM users を実行する
成功したら 200 で JSON を返す
エラーが出たらログを出して 500 を返す

という流れです。

重要なのは、「DBエラーの詳細をそのままクライアントに返さない」ことです。
内部情報(テーブル名・SQL文など)を外に漏らすのは、セキュリティ的に危険なので、
クライアントにはシンプルなメッセージだけ返します。

POST /api/users(ユーザー登録)

app.post('/api/users', async (req, res) => {
  try {
    const { name, email, password } = req.body;

    // 簡易バリデーション
    if (!name || !email || !password) {
      return res.status(400).json({ message: 'name, email, password は必須です' });
    }
    if (password.length < 8) {
      return res.status(400).json({ message: 'password は8文字以上にしてください' });
    }

    // すでに同じメールが使われていないかチェック
    const [existing] = await pool.execute(
      'SELECT id FROM users WHERE email = ?',
      [email]
    );
    if (existing.length > 0) {
      return res.status(400).json({ message: 'そのメールアドレスは既に使われています' });
    }

    // パスワードは本来ハッシュ化する(ここでは説明簡略化)
    const [result] = await pool.execute(
      'INSERT INTO users (name, email, password_hash, created_at, updated_at) VALUES (?, ?, ?, NOW(), NOW())',
      [name, email, password]
    );

    const createdUser = {
      id: result.insertId,
      name,
      email,
    };

    res.status(201).json(createdUser);
  } catch (err) {
    console.error(err);
    res.status(500).json({ message: 'Internal server error' });
  }
});
JavaScript

ここでの重要ポイントは多いので、少し深掘りします。

バリデーションをDBの前に置く

必須チェック・長さチェック・形式チェックなどは、
DBに触る前に弾きます。

これにより、

無駄なDBアクセスを減らせる
エラーメッセージをユーザーに分かりやすく返せる

というメリットがあります。

UNIQUE制約を“事前チェック+DB制約”の二重で守る

email の UNIQUE 制約は DB 側にもありますが、
API側でも事前に SELECT してチェックしています。

それでも、レースコンディション(同時登録)で
DB側の UNIQUE に引っかかる可能性はあるので、

最終的な砦は DB の制約
ユーザー体験のために API 側でもチェック

という二重構えが現実的です。

パスワードは必ずハッシュ化する

例では説明を簡単にするために生で入れていますが、
実務では絶対にハッシュ化します。

Node.js なら bcrypt などを使って、

const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(password, 10);
JavaScript

のようにしてから DB に保存します。

「パスワードを平文で保存しない」は、
API設計というより“人としての最低限のマナー”レベルです。


Python(FastAPI)+ MySQL でのAPI例

「型とスキーマでバリデーションを“宣言的に”書く」

Python 側の例として、モダンな FastAPI を使ったパターンも見てみます。
(Flask でも同じことはできますが、FastAPI はバリデーションが書きやすいです)

前提として、Day24 の接続コードを関数化しておきます。

# db.py
import os
import mysql.connector

def get_connection():
    return mysql.connector.connect(
        host=os.environ.get('DB_HOST', 'localhost'),
        port=int(os.environ.get('DB_PORT', '3306')),
        user=os.environ.get('DB_USER', 'sample_user'),
        password=os.environ.get('DB_PASSWORD', 'secret_password'),
        database=os.environ.get('DB_NAME', 'sample_app')
    )
Python

FastAPI アプリの骨格

# main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, constr
from db import get_connection

app = FastAPI()

class UserCreate(BaseModel):
    name: constr(min_length=1)
    email: EmailStr
    password: constr(min_length=8)

class UserResponse(BaseModel):
    id: int
    name: str
    email: EmailStr
Python

UserCreate が「リクエストボディの型」、
UserResponse が「レスポンスの型」です。

GET /api/users

@app.get("/api/users", response_model=list[UserResponse])
def list_users():
    conn = get_connection()
    cursor = conn.cursor(dictionary=True)
    cursor.execute("SELECT id, name, email FROM users ORDER BY id ASC")
    rows = cursor.fetchall()
    cursor.close()
    conn.close()
    return rows
Python

FastAPI が、返した辞書のリストを UserResponse に合わせて整形し、
自動的に JSON にして 200 で返してくれます。

POST /api/users

@app.post("/api/users", response_model=UserResponse, status_code=201)
def create_user(user: UserCreate):
    conn = get_connection()
    cursor = conn.cursor(dictionary=True)

    # すでに同じメールが使われていないかチェック
    cursor.execute("SELECT id FROM users WHERE email = %s", (user.email,))
    existing = cursor.fetchone()
    if existing:
        cursor.close()
        conn.close()
        raise HTTPException(status_code=400, detail="そのメールアドレスは既に使われています")

    # 本来はここでパスワードをハッシュ化する
    cursor.execute(
        """
        INSERT INTO users (name, email, password_hash, created_at, updated_at)
        VALUES (%s, %s, %s, NOW(), NOW())
        """,
        (user.name, user.email, user.password)
    )
    conn.commit()
    user_id = cursor.lastrowid

    cursor.close()
    conn.close()

    return UserResponse(id=user_id, name=user.name, email=user.email)
Python

ここでのポイントは、Node.js版とほぼ同じです。

リクエストボディは UserCreate 型で受ける(必須・形式・長さは自動バリデーション)
メールの重複はDBアクセスでチェックし、ダメなら 400 を返す
INSERT 後に commit し、作成されたユーザー情報を 201 で返す

FastAPI の良いところは、

バリデーションルールを「型」として宣言できる
エラー時に HTTPException を投げるだけで、JSONレスポンスを組んでくれる

という点です。


エラーハンドリングの“線引き”を意識する

「“ユーザーのせいのエラー”と“サーバーのせいのエラー”を分ける」

APIを作るときに、エラーを全部 500 にしてしまうのは簡単ですが、
それだとクライアント側が何も判断できません。

意識したい線引きはこうです。

ユーザーの入力が悪い → 400系(主に 400)
存在しないIDを指定した → 404
サーバー側のバグ・DB障害 → 500

例えば、「メールアドレスが既に使われている」は、
ユーザーが別のメールを選べば解決するので 400 です。

一方、「DB接続に失敗した」「SQL文が間違っている」は、
ユーザーにはどうしようもないので 500 です。

この線引きをちゃんとやると、

クライアント側の実装がシンプルになる
ログを見たときに「どっちの問題か」がすぐ分かる

というメリットがあります。


Day25 後半のまとめ

APIで「データ取得・登録処理」を実装するときは、まず HTTP の振る舞いとして、成功時には 200(取得)や 201(作成)を返し、入力がおかしいときは 400、存在しないリソースには 404、サーバー側のバグやDB障害には 500 を返す、というステータスコードの線引きを意識しながら、Node.js なら Express+mysql2、Python なら FastAPI+mysql-connector のような組み合わせで、GET /api/users では DB に対して SELECT を実行して結果を JSON で返し、POST /api/users では JSON ボディを受け取ってバリデーションし、メール重複チェックなどのビジネスルールを通過したらプレースホルダ付き INSERT を実行し、作成されたユーザー情報を返す、という流れをコードとして組み立てていく。
その際、「バリデーションはDBの前に」「UNIQUE制約はDBとAPIの二重構えで」「パスワードは必ずハッシュ化」「内部エラーの詳細は外に漏らさない」といったセキュリティと設計の基本を守ることで、APIを単なるSQLのラッパーではなく、「外部からのリクエストを安全に受け止めてDBを正しく使わせるための堅い入口」として機能させられるようになる。

タイトルとURLをコピーしました