第8回HTML解説 Node.jsでのAPI設計とデータベース連携の実践ガイド


前回の記事では、Node.jsを使って簡単なウェブサーバとRESTful APIを構築し、MongoDBと連携させる方法を紹介しました。今回は、その内容をさらに深掘りし、実際のアプリケーションで役立つ高度な技術について解説します。具体的には、認証機能の実装、エラーハンドリングデータバリデーション、およびAPI設計のベストプラクティスに焦点を当てていきます。

1. 認証機能の実装(JWTを使用)

ウェブアプリケーションにおいて、ユーザー認証は非常に重要な部分です。今回は、JSON Web Token(JWT)を使用した認証機能をNode.jsで実装する方法を解説します。JWTは、ユーザーの認証情報を安全にやり取りするための方法で、通常はユーザーのログイン時に発行され、クライアントとサーバ間での通信に使用されます。

1.1 JWTのインストール

まず、JWTを生成するためのjsonwebtokenというライブラリをインストールします。

npm install jsonwebtoken bcryptjs
  • jsonwebtoken: JWTを生成・検証するためのライブラリ。
  • bcryptjs: パスワードのハッシュ化を行うライブラリ。

1.2 ユーザー認証用のコード実装

認証機能の基本的な流れは以下のようになります:

  1. ユーザーがログインフォームで認証情報を入力。
  2. サーバーが入力された情報をデータベースと照合。
  3. 正しければJWTを発行し、クライアントに返す。
  4. クライアントはJWTを使用して、今後のリクエストに認証情報を含める。
サーバーコードの更新

server.jsに認証関連のエンドポイントを追加します。

const express = require('express');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const app = express();

// JSONリクエストボディを処理するためのミドルウェア
app.use(express.json());

// MongoDBへの接続
mongoose.connect('mongodb://127.0.0.1:27017/mydb', { useNewUrlParser: true, useUnifiedTopology: true })
    .then(() => console.log('Connected to MongoDB'))
    .catch((err) => console.log('Error connecting to MongoDB:', err));

// ユーザースキーマの定義
const userSchema = new mongoose.Schema({
    name: String,
    email: { type: String, unique: true },
    password: String,  // パスワードをハッシュ化して保存
});

// ユーザーのパスワードをハッシュ化して保存するためのミドルウェア
userSchema.pre('save', async function(next) {
    if (!this.isModified('password')) return next();
    this.password = await bcrypt.hash(this.password, 10);
    next();
});

// モデルの作成
const User = mongoose.model('User', userSchema);

// POST /signup - 新しいユーザーを作成
app.post('/signup', async (req, res) => {
    const { name, email, password } = req.body;

    // ユーザーが既に存在するか確認
    const userExists = await User.findOne({ email });
    if (userExists) {
        return res.status(400).json({ message: 'User already exists' });
    }

    // 新しいユーザーを作成
    const user = new User({ name, email, password });
    await user.save();
    res.status(201).json({ message: 'User created successfully' });
});

// POST /login - ユーザーをログインさせ、JWTを発行
app.post('/login', async (req, res) => {
    const { email, password } = req.body;

    // ユーザーを検索
    const user = await User.findOne({ email });
    if (!user) {
        return res.status(400).json({ message: 'Invalid credentials' });
    }

    // パスワードの照合
    const isMatch = await bcrypt.compare(password, user.password);
    if (!isMatch) {
        return res.status(400).json({ message: 'Invalid credentials' });
    }

    // JWTを発行
    const token = jwt.sign({ userId: user._id }, 'your_jwt_secret', { expiresIn: '1h' });

    // トークンをレスポンスとして返す
    res.json({ token });
});

// ミドルウェア: トークンの検証
const authenticateJWT = (req, res, next) => {
    const token = req.header('Authorization')?.replace('Bearer ', '');
    if (!token) {
        return res.status(403).json({ message: 'No token provided' });
    }

    jwt.verify(token, 'your_jwt_secret', (err, user) => {
        if (err) {
            return res.status(403).json({ message: 'Invalid token' });
        }
        req.user = user;
        next();
    });
};

// 認証が必要なAPI
app.get('/protected', authenticateJWT, (req, res) => {
    res.json({ message: 'This is a protected route', user: req.user });
});

// サーバの起動
app.listen(3000, () => {
    console.log('Server is running on http://127.0.0.1:3000');
});

1.3 認証フローの説明

  • /signup: 新しいユーザーを作成するエンドポイントです。ユーザーが登録時にパスワードはハッシュ化されて保存されます。
  • /login: ログインエンドポイントで、ユーザーの認証情報をチェックし、JWTを発行します。
  • authenticateJWT: リクエストに含まれるJWTを検証するミドルウェアです。認証されたユーザーのみがアクセスできるエンドポイントで使用します。
  • /protected: 認証されたユーザーのみがアクセスできる保護されたエンドポイントです。

1.4 JWTの使い方

ログイン後にクライアントは、JWTをAuthorizationヘッダーに含めて、保護されたAPIエンドポイントにリクエストを送信します。

Authorization: Bearer <JWT>

2. エラーハンドリング

Node.jsでは、エラーを適切に処理し、ユーザーにわかりやすいエラーメッセージを返すことが重要です。以下では、エラーハンドリングの基本的なアプローチと実装方法を紹介します。

2.1 グローバルエラーハンドリング

Expressにはエラーハンドリングのためのミドルウェアを簡単に追加できます。これにより、アプリケーション全体で発生するエラーを一元的に処理できます。

// グローバルエラーハンドリングミドルウェア
app.use((err, req, res, next) => {
    console.error(err.stack);  // エラーログをコンソールに出力
    res.status(500).json({ message: 'Internal Server Error' });
});

2.2 エラーの詳細な情報を提供

エラーメッセージは開発中は詳細にし、運用環境ではセキュリティ上の理由から詳細な情報を隠蔽します。開発環境と本番環境でエラーメッセージを適切に切り替えることが大切です。

app.use((err, req, res, next) => {
    const statusCode = err.statusCode || 500;
    const message = process.env.NODE_ENV === 'production' ? 'Internal Server Error' : err.message;
    res.status(statusCode).json({ message });
});

3. データバリデーション

入力されたデータが期待通りの形式であるかを検証することは、アプリケーションのセキュリティを強化するために重要です。これを行うために、Joiexpress-validatorなどのライブラリを使ってバリデーションを行います。

3.1 Joiを使ったバリ

デーション

まず、Joiをインストールします。

npm install joi

次に、ユーザー登録時に入力されたデータを検証します。

const Joi = require('joi');

// ユーザー登録データのバリデーション
const userSchema = Joi.object({
    name: Joi.string().min(3).required(),
    email: Joi.string().email().required(),
    password: Joi.string().min(6).required(),
});

// バリデーションの実行
app.post('/signup', async (req, res) => {
    const { error } = userSchema.validate(req.body);
    if (error) {
        return res.status(400).json({ message: error.details[0].message });
    }

    // バリデーション通過後の処理
});

これにより、入力データが不正な場合に詳細なエラーメッセージを提供できます。

4. API設計のベストプラクティス

  • ステータスコード: 適切なHTTPステータスコードを使用しましょう。成功時は200 OK201 Created、エラー時は400 Bad Request500 Internal Server Errorなどを返します。
  • エンドポイントの設計: エンドポイントはシンプルで直感的なものにしましょう。リソースに対する操作はHTTPメソッドで明確に分け、URLはRESTの規約に従いましょう。
  • バージョニング: APIのバージョン管理は重要です。APIを変更する際には、後方互換性を保つためにバージョニングを行いましょう。

5. まとめ

今回の記事では、Node.jsを使ったAPI設計をさらに発展させ、認証機能(JWT)、エラーハンドリング、データバリデーション、そしてAPI設計のベストプラクティスについて解説しました。これらを駆使することで、セキュアでスケーラブルなバックエンドを構築できます。

次回の記事では、さらに進んで、APIのテストや、パフォーマンスの最適化、さらにはフロントエンドとの連携についても触れていきたいと思いますので、ぜひご期待ください!

ここより下は最終的なコード



      必要なパッケージのインストール

      まずは、必要なパッケージをインストールします。

      npm install express mongoose jsonwebtoken bcryptjs joi
      

      server.jsのコード

      以下が、完成版のserver.jsのコードです。これにより、ユーザー管理と認証機能を備えたAPIが作成されます。

      const express = require('express');
      const mongoose = require('mongoose');
      const jwt = require('jsonwebtoken');
      const bcrypt = require('bcryptjs');
      const Joi = require('joi');
      const app = express();
      
      // JSONリクエストボディを処理するためのミドルウェア
      app.use(express.json());
      
      // MongoDBへの接続
      mongoose.connect('mongodb://127.0.0.1:27017/mydb', { useNewUrlParser: true, useUnifiedTopology: true })
          .then(() => console.log('Connected to MongoDB'))
          .catch((err) => console.log('Error connecting to MongoDB:', err));
      
      // ユーザースキーマの定義
      const userSchema = new mongoose.Schema({
          name: String,
          email: { type: String, unique: true },
          password: String,  // パスワードをハッシュ化して保存
      });
      
      // ユーザーのパスワードをハッシュ化して保存するためのミドルウェア
      userSchema.pre('save', async function(next) {
          if (!this.isModified('password')) return next();
          this.password = await bcrypt.hash(this.password, 10);
          next();
      });
      
      // モデルの作成
      const User = mongoose.model('User', userSchema);
      
      // Joiでユーザー登録データのバリデーション
      const userSchemaValidator = Joi.object({
          name: Joi.string().min(3).required(),
          email: Joi.string().email().required(),
          password: Joi.string().min(6).required(),
      });
      
      // POST /signup - 新しいユーザーを作成
      app.post('/signup', async (req, res) => {
          // バリデーション
          const { error } = userSchemaValidator.validate(req.body);
          if (error) {
              return res.status(400).json({ message: error.details[0].message });
          }
      
          const { name, email, password } = req.body;
      
          // ユーザーが既に存在するか確認
          const userExists = await User.findOne({ email });
          if (userExists) {
              return res.status(400).json({ message: 'User already exists' });
          }
      
          // 新しいユーザーを作成
          const user = new User({ name, email, password });
          await user.save();
          res.status(201).json({ message: 'User created successfully' });
      });
      
      // POST /login - ユーザーをログインさせ、JWTを発行
      app.post('/login', async (req, res) => {
          const { email, password } = req.body;
      
          // ユーザーを検索
          const user = await User.findOne({ email });
          if (!user) {
              return res.status(400).json({ message: 'Invalid credentials' });
          }
      
          // パスワードの照合
          const isMatch = await bcrypt.compare(password, user.password);
          if (!isMatch) {
              return res.status(400).json({ message: 'Invalid credentials' });
          }
      
          // JWTを発行
          const token = jwt.sign({ userId: user._id }, 'your_jwt_secret', { expiresIn: '1h' });
      
          // トークンをレスポンスとして返す
          res.json({ token });
      });
      
      // ミドルウェア: トークンの検証
      const authenticateJWT = (req, res, next) => {
          const token = req.header('Authorization')?.replace('Bearer ', '');
          if (!token) {
              return res.status(403).json({ message: 'No token provided' });
          }
      
          jwt.verify(token, 'your_jwt_secret', (err, user) => {
              if (err) {
                  return res.status(403).json({ message: 'Invalid token' });
              }
              req.user = user;
              next();
          });
      };
      
      // 認証が必要なAPI
      app.get('/protected', authenticateJWT, (req, res) => {
          res.json({ message: 'This is a protected route', user: req.user });
      });
      
      // グローバルエラーハンドリングミドルウェア
      app.use((err, req, res, next) => {
          const statusCode = err.statusCode || 500;
          const message = process.env.NODE_ENV === 'production' ? 'Internal Server Error' : err.message;
          res.status(statusCode).json({ message });
      });
      
      // サーバの起動
      app.listen(3000, () => {
          console.log('Server is running on http://127.0.0.1:3000');
      });
      

      説明

      1. ユーザー登録(/signup):
      • ユーザーがPOST /signupで登録する際、名前、メールアドレス、パスワードを受け取ります。
      • Joiを使って入力データを検証し、パスワードはbcryptjsでハッシュ化して保存します。
      • もしメールアドレスが既に登録されている場合、エラーメッセージを返します。
      1. ユーザーログイン(/login):
      • ユーザーがPOST /loginでログインする際、メールアドレスとパスワードを受け取ります。
      • メールアドレスがデータベースに存在するかを確認し、パスワードが一致する場合、JWT(JSON Web Token)を生成して返します。
      • JWTは、以降のリクエストでユーザー認証に使用されます。
      1. JWT認証ミドルウェア:
      • authenticateJWTミドルウェアは、リクエストヘッダーからJWTを抽出し、それを検証します。
      • 認証に成功すると、ユーザー情報がリクエストに追加され、次のミドルウェアまたはルートハンドラーに渡されます。
      1. 保護されたエンドポイント:
      • /protectedエンドポイントはJWTで認証されたユーザーのみがアクセスできるエンドポイントです。
      • トークンが有効であれば、ユーザー情報をレスポンスとして返します。
      1. エラーハンドリング:
      • app.useのエラーハンドリングミドルウェアを使用して、すべてのエラーを一元管理し、適切なHTTPステータスコードとメッセージを返します。
      • 環境に応じてエラーメッセージの詳細を変更しています。
      1. データバリデーション:
      • ユーザー登録時にJoiを使ってデータのバリデーションを行っています。これにより、リクエストデータが正しい形式であることを保証できます。

      実行方法

      1. MongoDBの準備:
      • MongoDBをインストールして、ローカルで実行しておきます(mongodb://127.0.0.1:27017/mydbで接続)。
      • データベースmydbが自動的に作成されます。
      1. サーバの起動:
      • 上記のserver.jsを保存したファイルがあるディレクトリで、以下のコマンドを実行します。
         node server.js
      
      1. PostmanやcURLを使ってAPIをテスト:
      • /signupで新しいユーザーを作成。
      • /loginでログインし、JWTを取得。
      • /protectedエンドポイントに対して、Authorization: Bearer <JWT>をヘッダーに追加してリクエストを送信します。

      コメントを残す

      メールアドレスが公開されることはありません。 が付いている欄は必須項目です