Next.jsでアプリケーションを構築したときに次のような機能が欲しいことがあった。

  • pagesディレクトリ以外でのルーティングの構築
  • アプリケーションよりも上位のレイヤに独自処理を挟む

これらの機能は、Next.jsのアプリケーションよりも上位で実行させる必要があるため、Next.js単体だと実現させるのが難しかった。

そこで、Next.jsのカスタムサーバとしてExpressを導入して機能の拡張を行った。

環境

導入

$ yarn add express

カスタムサーバの実行スクリプトを作成。

server.jsconst express = require('express');
const next = require('next');

const dev = process.env.NODE_ENV !== 'production';
const port = process.env.PORT || 3000;

const app = next({ dev });
const handler = app.getRequestHandler();
const server = express();

app.prepare()
    .then(() => {
        server.all('*', (req, res) => {
            return handler(req, res)
        });

        server.listen(port);
    })
    .catch(() => {
        console.error()
        process.exit(1)
    })

作成したファイルをNode.jsで実行するとExpressでHTTPサーバが起動する。

$ node server.js

Next.jsを開発モードで起動する場合は、NextServerのインスタンス(app)を生成する際にtrueを指定する。

実行環境は環境変数で切り替えられるようにして、次のようなスクリプトで指定できるようにした。

package.json"scripts": {
  "start:dev": "node server.js",
  "start:prod": "NODE_ENV=production node server.js"
}

静的ファイルのディレクトリを変更

Next.jsにおける静的ファイルの公開は、publicディレクトリが公開対象となるが、Expressを組み合わせることで公開ディレクトリを追加することができる。
https://expressjs.com/ja/starter/static-files.html

server.jsimport express from 'express'
import next from 'next'

const dev = process.env.NODE_ENV !== 'production';
const port = process.env.PORT || 3000;

const app = next({ dev });
const handler = app.getRequestHandler();
const server = express();

app.prepare()
    .then(() => {
        server.use('/static', express.static('static')); // staticディレクトリを公開ディクレトリに追加

        server.all('*', (req, res) => {
            return handler(req, res)
        });

        server.listen(port, () => {
            console.debug(`> Running on http://localhost:${port}`);
        })
    })
    .catch(() => {
        console.error()
        process.exit(1)
    })

独自処理の挿入

Expressでは必要に応じてミドルウェアを追加することで機能を拡張することができる。
https://expressjs.com/ja/guide/using-middleware.html

ここでは、例として管理者ページ(/admin)へアクセスされたときにユーザ認証を行う場合を想定とした処理を記載。

server.jsimport express, {NextFunction, Request} from 'express'
import next from 'next'

// server
const dev = process.env.NODE_ENV !== 'production'
const port = process.env.PORT || 3000;

const app = next({ dev })
const handler = app.getRequestHandler()
const server = express()

// ミドルウェア(auth)を定義
const auth = (req, res, next) => {
    const hasError = false; // 略(独自の認証処理 ex.JWT)
    if (hasError) {
        return next(res.status(401));
    }
    next();
}

app.prepare()
    .then(() => {
        // ミドルウェアの定義
        server.use((req, res, next) => {
            console.debug(`[${req.url}]`, `[${req.method}]`, 'Request Received')
            next()
        })

        // ミドルウェア(auth)を挿入
        server.get('/admin', auth, (req, res, next) => {
            return handler(req, res);
        })

        server.all('*', (req, res) => {
            return handler(req, res);
        })

        server.listen(port, () => {
            console.debug(`> Running on http://localhost:${port}`);
        })
    })
    .catch(() => {
        console.error()
        process.exit(1)
    })

このように記載することでNext.js側でルーティングされる前に任意の処理を挟むことができる。