サイトアイコンmaita tomoya dev io

Node.js

バックエンド基礎

Node.js

Node.jsとは何か

Node.jsはJavaScriptをサーバーサイドで実行するためのランタイム環境。通常、JavaScriptはブラウザの中でしか動かないが、Node.jsを使うことで、パソコン上でファイルの読み書きをしたり、Webサーバーを立てたり、データベースに接続したりできるようになる。

2009年にRyan Dahlが開発した。それまで「JavaScriptはフロントエンド専用」だったが、Node.jsの登場によってフロントエンドもバックエンドもJavaScriptで書ける時代が始まった。

V8エンジン

Node.jsの内部では、GoogleがChrome用に開発したV8 JavaScriptエンジンが動いている。V8はJavaScriptのコードを高速に実行するためのエンジンで、JavaScriptを直接マシンコード(CPUが理解できるコード)に変換する。

ユーザーが書いたJavaScript
       |
       v
   V8エンジン(コンパイル・最適化)
       |
       v
   マシンコード(CPUが直接実行)

つまり、Node.jsは「V8エンジン + サーバーサイド向け機能(ファイル操作、ネットワークなど)」をまとめたパッケージだと思えばよい。

イベント駆動・ノンブロッキングI/O

Node.jsの最大の特徴はイベント駆動かつノンブロッキングI/Oという仕組みにある。

レストランに例えてみよう。

ブロッキング(従来のサーバー): ウェイターが1人いて、1組の客の注文を受けたら料理が出来上がるまでその場で待ち続ける。他の客は待たされる。

ノンブロッキング(Node.js): ウェイターが1人いて、注文を受けたらキッチンに伝えてすぐ次の客の注文を取りに行く。料理ができたらキッチンから呼ばれて(イベント発火)、その客に料理を届ける。

【ブロッキングI/O】
リクエスト1 → [ファイル読み込み中...待機...完了] → レスポンス1
                                                   リクエスト2 → [DB問い合わせ中...待機...完了] → レスポンス2

【ノンブロッキングI/O(Node.js)】
リクエスト1 → ファイル読み込み開始 →(待たない)→ リクエスト2 → DB問い合わせ開始 →(待たない)→ ...
              ↓ 完了通知                           ↓ 完了通知
              レスポンス1                           レスポンス2

このおかげで、シングルスレッド(1つの処理の流れ)でありながら、多数の同時接続を効率よく処理できる。チャットアプリやリアルタイム通知など、大量の接続が発生するアプリケーションに特に向いている。

ブラウザJavaScriptとNode.jsの違い

同じJavaScriptでも、実行される場所によってできることが異なる。

比較項目ブラウザJSNode.js
実行場所ブラウザの中パソコン/サーバー上
DOM操作できる(document, windowなど)できない
ファイル操作できない(セキュリティ上)できる(fsモジュール)
ネットワークサーバー構築できないできる(httpモジュール)
グローバルオブジェクトwindowglobal / globalThis
プロセス情報なしprocessオブジェクトで取得可能
モジュールシステムES Modules(import/export)CommonJS(require)+ ES Modules
パッケージ管理なし(CDNやbundler経由)npm / yarn / pnpm
// ブラウザでは動くが、Node.jsではエラーになるコード
document.getElementById('app') // ReferenceError: document is not defined
window.alert('Hello') // ReferenceError: window is not defined

// Node.jsでは動くが、ブラウザではエラーになるコード
const fs = require('fs')
fs.readFileSync('./data.txt', 'utf-8') // ブラウザにはfsモジュールがない

// Node.js固有のオブジェクト
console.log(process.version) // Node.jsのバージョン
console.log(process.platform) // 'darwin', 'linux', 'win32'
console.log(process.cwd()) // 現在の作業ディレクトリ
console.log(process.env.HOME) // 環境変数の取得

インストール方法

nvm(Node Version Manager)を使う方法(推奨)

Node.jsはバージョンによって機能が異なるため、プロジェクトごとにバージョンを切り替える場面が多い。nvmを使うと、複数のバージョンを簡単にインストール・切り替えできる。

# nvmのインストール(macOS / Linux)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

# ターミナルを再起動してから確認
nvm --version

# Node.jsのLTS(Long Term Support = 長期サポート版)をインストール
nvm install --lts

# 特定のバージョンをインストール
nvm install 20
nvm install 18

# インストール済みのバージョン一覧
nvm ls

# バージョンの切り替え
nvm use 20
nvm use 18

# デフォルトのバージョンを設定
nvm alias default 20

# 現在のバージョン確認
node -v
npm -v

なぜnvmが推奨されるのか:

インストール方法メリットデメリット
公式サイトから直接簡単バージョン切り替えが面倒
nvm複数バージョン管理、切り替えが簡単初回セットアップが少し手間
Homebrew(macOS)簡単バージョン管理が難しい
Volta高速、プロジェクト単位で自動切り替え比較的新しいツール

プロジェクトのルートに.nvmrcファイルを置くと、チームで同じバージョンを使える。

# .nvmrcファイルの作成(バージョンを記載するだけ)
echo "20" > .nvmrc

# .nvmrcに書かれたバージョンに切り替え
nvm use

Node.jsとnpmの関係

Node.jsをインストールすると、npm(Node Package Manager) も一緒にインストールされる。npmはJavaScriptのパッケージ(ライブラリ)を管理するためのツール。

# npmのバージョン確認
npm -v

# パッケージの初期化(package.jsonを作成)
npm init -y

# パッケージのインストール
npm install express           # dependencies(本番で必要)
npm install --save-dev jest   # devDependencies(開発時のみ必要)

# グローバルインストール(コマンドとして使えるようになる)
npm install -g nodemon

Hello World

ファイルを実行する方法

// hello.js
console.log('Hello, Node.js!')
console.log('実行環境:', process.platform)
console.log('Node.jsバージョン:', process.version)
# 実行
node hello.js

# 出力:
# Hello, Node.js!
# 実行環境: darwin
# Node.jsバージョン: v20.x.x

REPL(Read-Eval-Print Loop)

REPLは「入力→評価→出力→繰り返し」の対話的な実行環境。ちょっとしたコードの動作確認に便利。

# REPLを起動
node

# 以下、対話的に実行できる
> 1 + 2
3
> 'Hello' + ' World'
'Hello World'
> const arr = [1, 2, 3]
undefined
> arr.map(x => x * 2)
[ 2, 4, 6 ]
> .exit  # REPLを終了

簡単なHTTPサーバー

Node.jsの醍醐味は、たった数行でWebサーバーを立てられること。

// server.js
const http = require('http')

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
  res.end('こんにちは、Node.jsサーバーです!')
})

server.listen(3000, () => {
  console.log('サーバーが http://localhost:3000 で起動しました')
})
node server.js
# ブラウザで http://localhost:3000 にアクセスすると表示される

モジュールシステム

Node.jsにはCommonJSES Modulesの2つのモジュールシステムがある。

CommonJS(require / module.exports)

Node.jsで従来から使われている方式。require()でモジュールを読み込み、module.exportsでエクスポートする。

// math.js(モジュール側)
function add(a, b) {
  return a + b
}

function multiply(a, b) {
  return a * b
}

module.exports = { add, multiply }
// app.js(使う側)
const { add, multiply } = require('./math')

console.log(add(2, 3)) // 5
console.log(multiply(4, 5)) // 20

ES Modules(import / export)

JavaScriptの標準仕様であるES Modulesも使える。Node.jsでES Modulesを使うには、以下のいずれかが必要。

  1. ファイル拡張子を.mjsにする
  2. package.json"type": "module"を追加する
// math.mjs(モジュール側)
export function add(a, b) {
  return a + b
}

export function multiply(a, b) {
  return a * b
}

export default function subtract(a, b) {
  return a - b
}
// app.mjs(使う側)
import subtract, { add, multiply } from './math.mjs'

console.log(add(2, 3)) // 5
console.log(multiply(4, 5)) // 20
console.log(subtract(10, 3)) // 7

CommonJS vs ES Modules 比較

項目CommonJSES Modules
構文require() / module.exportsimport / export
読み込みタイミング実行時(動的)パース時(静的)
トップレベルawait不可可能
ファイル拡張子.js(デフォルト).mjsまたは"type":"module"
循環参照部分的にサポート仕様で明確に定義
Tree Shaking難しい可能(静的解析できるため)
現在の推奨レガシーだが広く使われている新規プロジェクトでは推奨

実務では、古いプロジェクトはCommonJS、新しいプロジェクトはES Modulesを使う傾向にある。どちらも読めるようにしておくことが重要。

組み込みモジュール

Node.jsには最初から使えるモジュール(ビルトインモジュール)が多数用意されている。外部パッケージをインストールしなくても、基本的な機能はこれらで実現できる。

fs(ファイルシステム)

ファイルやディレクトリの操作を行う。

const fs = require('fs')

// --- ファイルの読み込み(非同期)---
fs.readFile('./data.txt', 'utf-8', (err, data) => {
  if (err) {
    console.error('読み込みエラー:', err.message)
    return
  }
  console.log('ファイル内容:', data)
})

// --- ファイルの読み込み(同期)---
try {
  const data = fs.readFileSync('./data.txt', 'utf-8')
  console.log('ファイル内容:', data)
} catch (err) {
  console.error('読み込みエラー:', err.message)
}

// --- ファイルの読み込み(Promiseベース、推奨)---
const fsPromises = require('fs').promises
// または: const fsPromises = require('fs/promises'); // Node.js 14以降

async function readData() {
  try {
    const data = await fsPromises.readFile('./data.txt', 'utf-8')
    console.log('ファイル内容:', data)
  } catch (err) {
    console.error('読み込みエラー:', err.message)
  }
}
readData()

// --- ファイルの書き込み ---
fs.writeFile('./output.txt', 'Hello, World!', (err) => {
  if (err) {
    console.error('書き込みエラー:', err.message)
    return
  }
  console.log('ファイルに書き込みました')
})

// --- ファイルの追記 ---
fs.appendFile('./log.txt', '新しいログ行\n', (err) => {
  if (err) console.error(err)
})

// --- ディレクトリの作成 ---
fs.mkdir('./new-dir', { recursive: true }, (err) => {
  if (err) console.error(err)
})

// --- ファイル一覧の取得 ---
fs.readdir('./', (err, files) => {
  if (err) console.error(err)
  console.log('ファイル一覧:', files)
})

// --- ファイルの存在確認 ---
console.log(fs.existsSync('./data.txt')) // true or false

// --- ファイル情報の取得 ---
fs.stat('./data.txt', (err, stats) => {
  if (err) console.error(err)
  console.log('ファイルサイズ:', stats.size, 'bytes')
  console.log('最終更新:', stats.mtime)
  console.log('ディレクトリか:', stats.isDirectory())
})

path(パス操作)

OS間のパスの違い(Windowsは\、macOS/Linuxは/)を吸収してくれるモジュール。

const path = require('path')

// パスの結合(OSに応じた区切り文字を使ってくれる)
console.log(path.join('/users', 'taro', 'documents', 'file.txt'))
// → /users/taro/documents/file.txt

// 絶対パスの取得
console.log(path.resolve('src', 'index.js'))
// → /current/working/directory/src/index.js

// ファイル名の取得
console.log(path.basename('/users/taro/photo.jpg')) // photo.jpg
console.log(path.basename('/users/taro/photo.jpg', '.jpg')) // photo

// 拡張子の取得
console.log(path.extname('report.pdf')) // .pdf
console.log(path.extname('archive.tar.gz')) // .gz

// ディレクトリ名の取得
console.log(path.dirname('/users/taro/photo.jpg')) // /users/taro

// パスのパース(分解)
console.log(path.parse('/users/taro/photo.jpg'))
// {
//   root: '/',
//   dir: '/users/taro',
//   base: 'photo.jpg',
//   ext: '.jpg',
//   name: 'photo'
// }

http(HTTPサーバー/クライアント)

const http = require('http')

// --- サーバーの作成 ---
const server = http.createServer((req, res) => {
  // リクエスト情報
  console.log(`${req.method} ${req.url}`)

  // ルーティング
  if (req.url === '/' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ message: 'トップページ' }))
  } else if (req.url === '/about' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
    res.end('Aboutページ')
  } else {
    res.writeHead(404, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ error: 'ページが見つかりません' }))
  }
})

server.listen(3000, () => {
  console.log('http://localhost:3000 で起動中')
})

os(OS情報)

const os = require('os')

console.log('ホスト名:', os.hostname())
console.log('OS:', os.platform()) // darwin, linux, win32
console.log('アーキテクチャ:', os.arch()) // x64, arm64
console.log('CPUコア数:', os.cpus().length)
console.log('メモリ(合計):', Math.round(os.totalmem() / 1024 / 1024 / 1024), 'GB')
console.log('メモリ(空き):', Math.round(os.freemem() / 1024 / 1024 / 1024), 'GB')
console.log('ホームディレクトリ:', os.homedir())
console.log('一時ディレクトリ:', os.tmpdir())

url(URL解析)

const { URL } = require('url')

const myUrl = new URL('https://example.com:8080/api/users?page=2&sort=name#section1')

console.log('プロトコル:', myUrl.protocol) // https:
console.log('ホスト:', myUrl.host) // example.com:8080
console.log('ホスト名:', myUrl.hostname) // example.com
console.log('ポート:', myUrl.port) // 8080
console.log('パス:', myUrl.pathname) // /api/users
console.log('クエリ:', myUrl.search) // ?page=2&sort=name
console.log('ハッシュ:', myUrl.hash) // #section1

// クエリパラメータの取得
console.log('page:', myUrl.searchParams.get('page')) // 2
console.log('sort:', myUrl.searchParams.get('sort')) // name

crypto(暗号化)

const crypto = require('crypto')

// ランダムな文字列の生成(トークンやIDに使える)
const token = crypto.randomBytes(32).toString('hex')
console.log('トークン:', token)

// UUIDの生成
const uuid = crypto.randomUUID()
console.log('UUID:', uuid)

// ハッシュの生成(パスワードのハッシュ化などに使う)
const hash = crypto.createHash('sha256').update('password123').digest('hex')
console.log('SHA256ハッシュ:', hash)

// HMAC(メッセージ認証コード)
const hmac = crypto.createHmac('sha256', 'secret-key').update('message').digest('hex')
console.log('HMAC:', hmac)

events(イベントエミッター)

Node.jsのイベント駆動の基盤となるモジュール。

const EventEmitter = require('events')

// イベントエミッターのインスタンスを作成
const emitter = new EventEmitter()

// イベントリスナーの登録
emitter.on('order', (item, quantity) => {
  console.log(`注文を受けました: ${item} x ${quantity}`)
})

emitter.on('order', (item, quantity) => {
  console.log(`在庫を確認します: ${item}`)
})

// 1回だけ実行されるリスナー
emitter.once('welcome', (name) => {
  console.log(`ようこそ、${name}さん!`)
})

// イベントの発火
emitter.emit('order', 'コーヒー', 2)
// 出力:
// 注文を受けました: コーヒー x 2
// 在庫を確認します: コーヒー

emitter.emit('welcome', '太郎') // ようこそ、太郎さん!
emitter.emit('welcome', '花子') // (何も出力されない。onceなので1回だけ)

組み込みモジュール一覧(主要なもの)

モジュール用途よく使う場面
fsファイルシステム操作ファイルの読み書き、ディレクトリ操作
pathパス操作ファイルパスの結合・解析
http / httpsHTTP通信サーバー構築、APIリクエスト
osOS情報システム情報の取得
urlURL解析URLのパース、クエリパラメータ操作
crypto暗号化ハッシュ生成、トークン生成
eventsイベント処理カスタムイベントの作成
streamストリーム処理大量データの逐次処理
child_process子プロセス外部コマンドの実行
utilユーティリティデバッグ、フォーマット
bufferバイナリデータバイナリデータの操作
zlib圧縮/解凍gzip圧縮
readline行単位の入力CLI対話ツールの作成

ファイルの読み書き(詳細)

ファイルの読み書きには3つのスタイルがある。それぞれの違いを理解しておくことが重要。

同期(Sync) vs 非同期(コールバック) vs 非同期(Promise/async-await)

const fs = require('fs')
const fsPromises = require('fs').promises

// ========================================
// 1. 同期(Sync)- 処理が完了するまで次に進まない
// ========================================
console.log('--- 同期処理 ---')
console.log('読み込み開始')
const data1 = fs.readFileSync('./sample.txt', 'utf-8')
console.log('内容:', data1)
console.log('読み込み完了') // 必ず上の行の後に実行される

// 出力順: 読み込み開始 → 内容:... → 読み込み完了

// ========================================
// 2. 非同期(コールバック)- 処理の完了を待たない
// ========================================
console.log('--- コールバック ---')
console.log('読み込み開始')
fs.readFile('./sample.txt', 'utf-8', (err, data) => {
  if (err) throw err
  console.log('内容:', data)
})
console.log('読み込み完了(のはず)') // 先にこちらが実行される!

// 出力順: 読み込み開始 → 読み込み完了(のはず)→ 内容:...

// ========================================
// 3. 非同期(async/await)- 見た目は同期だが非同期 ★推奨★
// ========================================
async function readFileAsync() {
  console.log('--- async/await ---')
  console.log('読み込み開始')
  const data = await fsPromises.readFile('./sample.txt', 'utf-8')
  console.log('内容:', data)
  console.log('読み込み完了')
}
readFileAsync()

// 出力順: 読み込み開始 → 内容:... → 読み込み完了

どれを使うべきか:

スタイル使う場面注意点
同期(Sync)起動時の設定ファイル読み込みサーバー処理中に使うとブロックされる
コールバックレガシーコードコールバック地獄になりやすい
async/await新規コード全般推奨。読みやすく、エラーハンドリングも容易

実践的なファイル操作

const fs = require('fs').promises
const path = require('path')

// JSONファイルの読み書き
async function manageConfig() {
  const configPath = path.join(__dirname, 'config.json')

  // JSONファイルの読み込み
  const raw = await fs.readFile(configPath, 'utf-8')
  const config = JSON.parse(raw)
  console.log('現在の設定:', config)

  // 設定の変更
  config.port = 4000
  config.debug = true

  // JSONファイルの書き込み(整形して保存)
  await fs.writeFile(configPath, JSON.stringify(config, null, 2))
  console.log('設定を更新しました')
}

// ディレクトリ内のファイルを再帰的に一覧表示
async function listFiles(dirPath, indent = '') {
  const items = await fs.readdir(dirPath, { withFileTypes: true })

  for (const item of items) {
    const fullPath = path.join(dirPath, item.name)
    if (item.isDirectory()) {
      console.log(`${indent}[DIR] ${item.name}/`)
      await listFiles(fullPath, indent + '  ')
    } else {
      const stats = await fs.stat(fullPath)
      const sizeKB = (stats.size / 1024).toFixed(1)
      console.log(`${indent}${item.name} (${sizeKB} KB)`)
    }
  }
}

// CSVファイルの簡易パース
async function parseCSV(filePath) {
  const content = await fs.readFile(filePath, 'utf-8')
  const lines = content.trim().split('\n')
  const headers = lines[0].split(',')
  const data = lines.slice(1).map((line) => {
    const values = line.split(',')
    return headers.reduce((obj, header, i) => {
      obj[header.trim()] = values[i].trim()
      return obj
    }, {})
  })
  return data
}

HTTPサーバーの作成

素のNode.js vs Express.js

Node.jsの組み込みhttpモジュールだけでもサーバーは作れるが、ルーティングやリクエストボディの解析など、全て自分で実装する必要がある。Express.jsはそういった面倒な処理を簡単にしてくれるフレームワーク。

// ====================================
// 素のNode.jsでAPIサーバーを作る場合
// ====================================
const http = require('http')

const server = http.createServer((req, res) => {
  // リクエストボディを自分で収集する必要がある
  let body = ''
  req.on('data', (chunk) => {
    body += chunk
  })
  req.on('end', () => {
    // ルーティングも自分で分岐
    if (req.method === 'GET' && req.url === '/api/users') {
      res.writeHead(200, { 'Content-Type': 'application/json' })
      res.end(JSON.stringify([{ id: 1, name: '太郎' }]))
    } else if (req.method === 'POST' && req.url === '/api/users') {
      const user = JSON.parse(body)
      res.writeHead(201, { 'Content-Type': 'application/json' })
      res.end(JSON.stringify({ id: 2, ...user }))
    } else {
      res.writeHead(404)
      res.end('Not Found')
    }
  })
})

server.listen(3000)
// ====================================
// Express.jsで同じことをする場合
// ====================================
const express = require('express')
const app = express()

app.use(express.json()) // JSONボディを自動解析

app.get('/api/users', (req, res) => {
  res.json([{ id: 1, name: '太郎' }])
})

app.post('/api/users', (req, res) => {
  const user = req.body // 自動でパースされている
  res.status(201).json({ id: 2, ...user })
})

app.listen(3000)

見比べると、Express.jsの方がはるかにシンプルで読みやすいことがわかる。

Express.js入門

インストールとプロジェクトセットアップ

# プロジェクトディレクトリの作成
mkdir my-api && cd my-api

# package.jsonの初期化
npm init -y

# Expressのインストール
npm install express

# 開発用にnodemonをインストール(ファイル変更で自動再起動)
npm install --save-dev nodemon
// package.json に追加
{
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  }
}

ルーティング

const express = require('express')
const app = express()

// GETリクエスト
app.get('/', (req, res) => {
  res.send('トップページ')
})

// パスパラメータ(:idの部分が動的に変わる)
app.get('/users/:id', (req, res) => {
  const userId = req.params.id
  res.json({ id: userId, name: `ユーザー${userId}` })
})

// クエリパラメータ(?key=value の形式)
// 例: /search?q=Node.js&page=2
app.get('/search', (req, res) => {
  const query = req.query.q
  const page = req.query.page || 1
  res.json({ query, page })
})

// POSTリクエスト
app.use(express.json()) // JSONボディパーサー
app.post('/users', (req, res) => {
  const { name, email } = req.body
  res.status(201).json({ id: Date.now(), name, email })
})

// PUTリクエスト(全体更新)
app.put('/users/:id', (req, res) => {
  const { id } = req.params
  const { name, email } = req.body
  res.json({ id, name, email, updated: true })
})

// DELETEリクエスト
app.delete('/users/:id', (req, res) => {
  const { id } = req.params
  res.json({ message: `ユーザー${id}を削除しました` })
})

// ルーターの分割(大規模アプリ向け)
const userRouter = express.Router()
userRouter.get('/', (req, res) => res.json({ users: [] }))
userRouter.get('/:id', (req, res) => res.json({ id: req.params.id }))
userRouter.post('/', (req, res) => res.status(201).json(req.body))

app.use('/api/users', userRouter) // /api/users 以下のルートをまとめて登録

app.listen(3000, () => {
  console.log('http://localhost:3000 で起動中')
})

ミドルウェア

ミドルウェアはExpress.jsの核となる概念。リクエストが来てからレスポンスを返すまでの間に、順番に実行される関数のチェーンのこと。

リクエスト → [ログ記録] → [認証チェック] → [ボディ解析] → [ルートハンドラ] → レスポンス
              ↑ミドルウェア1  ↑ミドルウェア2   ↑ミドルウェア3    ↑最終処理

各ミドルウェアは(req, res, next)の3つの引数を受け取る。next()を呼ぶと次のミドルウェアに処理が渡る。

const express = require('express')
const app = express()

// ========================================
// アプリケーション全体に適用するミドルウェア
// ========================================

// ログ記録ミドルウェア
app.use((req, res, next) => {
  const timestamp = new Date().toISOString()
  console.log(`[${timestamp}] ${req.method} ${req.url}`)
  next() // 次のミドルウェアへ
})

// リクエスト処理時間の計測
app.use((req, res, next) => {
  const start = Date.now()
  res.on('finish', () => {
    const duration = Date.now() - start
    console.log(`処理時間: ${duration}ms`)
  })
  next()
})

// 組み込みミドルウェア
app.use(express.json()) // JSONボディのパース
app.use(express.urlencoded({ extended: true })) // フォームデータのパース
app.use(express.static('public')) // 静的ファイルの配信

// ========================================
// 特定のルートにのみ適用するミドルウェア
// ========================================

// 認証チェックミドルウェア
function authMiddleware(req, res, next) {
  const token = req.headers.authorization
  if (!token) {
    return res.status(401).json({ error: '認証が必要です' })
  }
  // トークンの検証処理...
  req.userId = 'decoded-user-id' // 検証結果をreqに追加
  next()
}

// 認証が必要なルートにのみ適用
app.get('/api/profile', authMiddleware, (req, res) => {
  res.json({ userId: req.userId, name: 'テストユーザー' })
})

// 複数のミドルウェアを連結
function validateInput(req, res, next) {
  if (!req.body.name) {
    return res.status(400).json({ error: 'nameは必須です' })
  }
  next()
}

app.post('/api/users', authMiddleware, validateInput, (req, res) => {
  res.status(201).json({ message: 'ユーザーを作成しました' })
})

// ========================================
// エラーハンドリングミドルウェア(引数が4つ)
// ========================================
app.use((err, req, res, next) => {
  console.error('エラー発生:', err.message)
  res.status(err.status || 500).json({
    error: {
      message: err.message,
      status: err.status || 500,
    },
  })
})

app.listen(3000)

静的ファイル配信

const express = require('express')
const path = require('path')
const app = express()

// publicディレクトリ内のファイルをそのまま配信
// public/index.html → http://localhost:3000/index.html
// public/css/style.css → http://localhost:3000/css/style.css
app.use(express.static(path.join(__dirname, 'public')))

// パスプレフィックスを付けることもできる
// public/logo.png → http://localhost:3000/static/logo.png
app.use('/static', express.static(path.join(__dirname, 'public')))

app.listen(3000)

環境変数

アプリケーションの設定値(データベースのURL、APIキー、ポート番号など)はコードに直接書かず、環境変数で管理するのが鉄則。

process.env

// Node.jsの組み込み機能で環境変数にアクセスできる
console.log(process.env.NODE_ENV) // 'development' or 'production'
console.log(process.env.PORT) // '3000'
console.log(process.env.HOME) // '/Users/taro'

// 実行時に環境変数を指定
// PORT=4000 NODE_ENV=production node app.js

dotenvパッケージ

.envファイルに設定値を書き、dotenvパッケージで読み込むのが一般的。

npm install dotenv
# .env(プロジェクトルートに配置)
PORT=3000
NODE_ENV=development
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=my-super-secret-key-change-in-production
API_KEY=abc123xyz
// app.js(ファイルの先頭で読み込む)
require('dotenv').config()

// これで.envの値がprocess.envに追加される
const port = process.env.PORT || 3000
const dbUrl = process.env.DATABASE_URL
const jwtSecret = process.env.JWT_SECRET

console.log(`ポート: ${port}`)
console.log(`DB: ${dbUrl}`)

重要: .envファイルはGitにコミットしてはいけない。 .gitignoreに追加すること。

# .gitignore
.env
.env.local
.env.production

代わりに.env.exampleというテンプレートファイルを用意しておくと、他の開発者がどんな環境変数が必要かわかる。

# .env.example(値は空にしてコミットする)
PORT=3000
NODE_ENV=development
DATABASE_URL=
JWT_SECRET=
API_KEY=

イベントループの仕組み

Node.jsがシングルスレッドなのに高速な理由を理解するには、イベントループの仕組みを知る必要がある。

全体像

   ┌───────────────────────────┐
   │      Call Stack           │  ← 現在実行中のコード
   │   (コールスタック)          │
   └─────────┬─────────────────┘
             │
             │ 非同期処理を依頼
             ▼
   ┌───────────────────────────┐
   │     Node.js APIs          │  ← fs, http, setTimeout等
   │  (バックグラウンド処理)      │     別スレッドで処理される
   └─────────┬─────────────────┘
             │
             │ 完了したコールバックをキューに追加
             ▼
   ┌───────────────────────────┐
   │   Microtask Queue         │  ← Promise.then, process.nextTick
   │ (マイクロタスクキュー)      │     ★ 優先度が高い
   ├───────────────────────────┤
   │   Task Queue              │  ← setTimeout, setInterval, I/O
   │ (タスクキュー/マクロタスク)  │     通常の優先度
   └─────────┬─────────────────┘
             │
             │ イベントループがキューから取り出して実行
             ▼
   ┌───────────────────────────┐
   │      Call Stack           │  ← コールバックが実行される
   └───────────────────────────┘

実行順序の具体例

console.log('1: 同期処理(最初)')

setTimeout(() => {
  console.log('2: setTimeout(タスクキュー)')
}, 0)

Promise.resolve().then(() => {
  console.log('3: Promise.then(マイクロタスクキュー)')
})

process.nextTick(() => {
  console.log('4: process.nextTick(マイクロタスクキュー、最優先)')
})

console.log('5: 同期処理(最後)')

// 出力順:
// 1: 同期処理(最初)
// 5: 同期処理(最後)
// 4: process.nextTick(マイクロタスクキュー、最優先)
// 3: Promise.then(マイクロタスクキュー)
// 2: setTimeout(タスクキュー)

実行順序のルール:

優先度種類
1(最高)同期コード通常のJavaScriptコード
2process.nextTickprocess.nextTick(callback)
3マイクロタスクPromise.then(), queueMicrotask()
4マクロタスクsetTimeout(), setInterval(), I/Oコールバック

イベントループのフェーズ

イベントループは以下のフェーズを繰り返し回り続ける。

   ┌──────────────────────────────┐
   │         timers               │  setTimeout, setIntervalのコールバック
   ├──────────────────────────────┤
   │    pending callbacks         │  I/Oコールバック
   ├──────────────────────────────┤
   │       idle, prepare          │  内部使用
   ├──────────────────────────────┤
   │         poll                 │  新しいI/Oイベントを取得、I/Oコールバック実行
   ├──────────────────────────────┤
   │         check                │  setImmediateのコールバック
   ├──────────────────────────────┤
   │    close callbacks           │  close系のコールバック
   └──────────────────────────────┘

   ※ 各フェーズの間にマイクロタスクキューが処理される

ストリーム

ストリームは、大量のデータを一度にメモリに読み込まず、少しずつ処理する仕組み

動画配信サービスを思い浮かべてほしい。映画を全部ダウンロードしてから再生するのではなく、少しずつ受信しながら再生している。それがストリームの考え方。

4種類のストリーム

種類説明
Readableデータを読み取るストリームファイル読み込み、HTTPリクエスト
Writableデータを書き込むストリームファイル書き込み、HTTPレスポンス
Duplex読み書き両方TCPソケット
Transformデータを変換しながら読み書きgzip圧縮、暗号化

ストリームの使用例

const fs = require('fs')
const zlib = require('zlib')

// ========================================
// 通常の方法(メモリに全て載せる)- 小さいファイル向け
// ========================================
// 1GBのファイルを読むと、1GBのメモリを消費する!
const data = fs.readFileSync('large-file.txt', 'utf-8')
console.log(data.length)

// ========================================
// ストリーム(少しずつ処理)- 大きいファイル向け
// ========================================
const readStream = fs.createReadStream('large-file.txt', {
  encoding: 'utf-8',
  highWaterMark: 64 * 1024, // 64KBずつ読み込む
})

let totalSize = 0
readStream.on('data', (chunk) => {
  totalSize += chunk.length
  console.log(`チャンク受信: ${chunk.length} bytes`)
})

readStream.on('end', () => {
  console.log(`合計: ${totalSize} bytes`)
})

readStream.on('error', (err) => {
  console.error('エラー:', err.message)
})

// ========================================
// パイプ(ストリーム同士を接続する)
// ========================================

// ファイルのコピー
fs.createReadStream('input.txt').pipe(fs.createWriteStream('output.txt'))

// ファイルの圧縮
fs.createReadStream('large-file.txt')
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream('large-file.txt.gz'))

// ファイルの解凍
fs.createReadStream('large-file.txt.gz')
  .pipe(zlib.createGunzip())
  .pipe(fs.createWriteStream('large-file-restored.txt'))

// ========================================
// HTTPレスポンスでストリームを使う
// ========================================
const http = require('http')

http
  .createServer((req, res) => {
    // 大きなファイルをストリームで返す(メモリ効率が良い)
    const fileStream = fs.createReadStream('./large-file.txt')
    res.writeHead(200, { 'Content-Type': 'text/plain' })
    fileStream.pipe(res)
  })
  .listen(3000)

Transformストリーム

データを変換しながら流すストリーム。

const { Transform } = require('stream')

// 大文字に変換するTransformストリーム
const upperCase = new Transform({
  transform(chunk, encoding, callback) {
    const upper = chunk.toString().toUpperCase()
    callback(null, upper)
  },
})

// 使い方: 入力 → 大文字変換 → 出力
process.stdin.pipe(upperCase).pipe(process.stdout)

// CSVの行を加工するTransformストリーム
const csvTransform = new Transform({
  transform(chunk, encoding, callback) {
    const lines = chunk.toString().split('\n')
    const transformed = lines
      .map((line) => line.trim())
      .filter((line) => line.length > 0)
      .map((line) => `[処理済] ${line}`)
      .join('\n')
    callback(null, transformed + '\n')
  },
})

エラーハンドリング

try/catch(同期処理・async/await)

// 同期処理のエラーハンドリング
try {
  const data = JSON.parse('invalid json')
} catch (err) {
  console.error('パースエラー:', err.message)
}

// async/awaitのエラーハンドリング
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data')
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`)
    }
    const data = await response.json()
    return data
  } catch (err) {
    console.error('データ取得エラー:', err.message)
    throw err // 必要に応じて再スロー
  }
}

Express.jsのエラーミドルウェア

const express = require('express')
const app = express()

// カスタムエラークラス
class AppError extends Error {
  constructor(message, statusCode) {
    super(message)
    this.statusCode = statusCode
    this.isOperational = true // 予期されたエラーかどうか
  }
}

// ルートでエラーをスロー
app.get('/api/users/:id', async (req, res, next) => {
  try {
    const user = await findUser(req.params.id)
    if (!user) {
      throw new AppError('ユーザーが見つかりません', 404)
    }
    res.json(user)
  } catch (err) {
    next(err) // エラーミドルウェアに渡す
  }
})

// async関数のエラーを自動でキャッチするラッパー
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next)
  }
}

// ラッパーを使うとtry/catchが不要になる
app.get(
  '/api/posts',
  asyncHandler(async (req, res) => {
    const posts = await getPosts()
    res.json(posts)
  })
)

// エラーハンドリングミドルウェア(必ず4つの引数を持つ)
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500
  const message = err.isOperational ? err.message : 'サーバー内部エラー'

  // 開発環境ではスタックトレースも返す
  const response = {
    error: {
      message,
      status: statusCode,
    },
  }

  if (process.env.NODE_ENV === 'development') {
    response.error.stack = err.stack
  }

  console.error(`[ERROR] ${statusCode} ${err.message}`)
  res.status(statusCode).json(response)
})

app.listen(3000)

未処理の例外・Promiseリジェクションのキャッチ

// 未キャッチの例外をキャッチ
process.on('uncaughtException', (err) => {
  console.error('未キャッチの例外:', err)
  process.exit(1) // プロセスを終了する(再起動はpm2等に任せる)
})

// 未処理のPromiseリジェクションをキャッチ
process.on('unhandledRejection', (reason, promise) => {
  console.error('未処理のPromiseリジェクション:', reason)
  process.exit(1)
})

デバッグ方法

console.logによるデバッグ

最も手軽だが、量が増えると管理が大変になる。

// 基本
console.log('値:', variable)

// オブジェクトの中身を深くまで表示
console.dir(deepObject, { depth: null, colors: true })

// テーブル形式で表示
console.table([
  { name: '太郎', age: 25 },
  { name: '花子', age: 30 },
])

// 処理時間の計測
console.time('DB問い合わせ')
await queryDatabase()
console.timeEnd('DB問い合わせ') // DB問い合わせ: 45.123ms

// スタックトレースの表示
console.trace('ここを通過')

// 条件付きログ
console.assert(user !== null, 'userがnullです!')

--inspectフラグ(Chrome DevTools)

# デバッグモードで起動
node --inspect app.js

# 最初の行で停止(ブレークポイントを設定してから実行したい場合)
node --inspect-brk app.js

起動後、Chromeで chrome://inspect にアクセスすると、Chrome DevToolsでデバッグできる。ブレークポイントの設定、変数の確認、ステップ実行が可能。

VSCodeでのデバッグ

.vscode/launch.jsonを作成する。

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Node.jsアプリを起動",
      "program": "${workspaceFolder}/app.js",
      "envFile": "${workspaceFolder}/.env"
    },
    {
      "type": "node",
      "request": "launch",
      "name": "現在のファイルを実行",
      "program": "${file}"
    }
  ]
}

VSCodeの左サイドバーにあるデバッグアイコンをクリックし、設定を選んで実行すれば、エディタ上でブレークポイントを設定してデバッグできる。

セキュリティ基礎

Webアプリケーションを公開する際は、セキュリティ対策が不可欠。

helmet(HTTPヘッダーのセキュリティ強化)

npm install helmet
const express = require('express')
const helmet = require('helmet')
const app = express()

// helmetを使うだけで、複数のセキュリティヘッダーが設定される
app.use(helmet())

// 設定されるヘッダーの例:
// X-Content-Type-Options: nosniff(MIMEタイプスニッフィング防止)
// X-Frame-Options: SAMEORIGIN(クリックジャッキング防止)
// Strict-Transport-Security(HTTPS強制)
// Content-Security-Policy(XSS防止)

cors(Cross-Origin Resource Sharing)

npm install cors
const cors = require('cors')

// 全てのオリジンを許可(開発時のみ)
app.use(cors())

// 特定のオリジンのみ許可(本番環境)
app.use(
  cors({
    origin: ['https://myapp.com', 'https://admin.myapp.com'],
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true, // Cookieを含むリクエストを許可
  })
)

Rate Limiting(レート制限)

npm install express-rate-limit
const rateLimit = require('express-rate-limit')

// 15分間に100リクエストまで
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 100,
  message: {
    error: 'リクエスト数が上限に達しました。しばらく待ってからお試しください。',
  },
  standardHeaders: true,
  legacyHeaders: false,
})

app.use('/api/', limiter)

// ログイン用(より厳しい制限)
const loginLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1時間
  max: 5, // 5回まで
  message: { error: 'ログイン試行回数が上限に達しました。1時間後にお試しください。' },
})

app.post('/api/login', loginLimiter, loginHandler)

入力バリデーション

ユーザーからの入力は絶対に信用しない。必ずサーバー側で検証する。

// 簡易的なバリデーション
function validateUser(data) {
  const errors = []

  if (!data.name || typeof data.name !== 'string') {
    errors.push('nameは必須の文字列です')
  } else if (data.name.length < 2 || data.name.length > 50) {
    errors.push('nameは2文字以上50文字以下です')
  }

  if (!data.email || !data.email.includes('@')) {
    errors.push('有効なメールアドレスを入力してください')
  }

  if (data.age !== undefined) {
    if (typeof data.age !== 'number' || data.age < 0 || data.age > 150) {
      errors.push('ageは0以上150以下の数値です')
    }
  }

  return errors
}

app.post('/api/users', (req, res) => {
  const errors = validateUser(req.body)
  if (errors.length > 0) {
    return res.status(400).json({ errors })
  }
  // バリデーション通過後の処理...
})

その他のセキュリティ対策

対策説明方法
SQLインジェクション防止悪意あるSQL文の挿入を防ぐプリペアドステートメントを使う
XSS防止悪意あるスクリプトの実行を防ぐ出力のエスケープ、CSPヘッダー
CSRF対策偽造リクエストの送信を防ぐCSRFトークン、SameSite Cookie
HTTPS通信の暗号化SSL/TLS証明書の導入
依存パッケージの脆弱性既知の脆弱性を持つパッケージnpm auditで定期的にチェック
エラーメッセージ内部情報の漏洩防止本番ではスタックトレースを隠す
# 依存パッケージの脆弱性チェック
npm audit

# 自動修正
npm audit fix

まとめ

Node.jsの学習で押さえるべきポイントを整理する。

段階学ぶべき内容
基礎Node.jsの実行、モジュールシステム、ファイル操作
サーバー構築httpモジュール、Express.js、ルーティング、ミドルウェア
データ処理ストリーム、環境変数、JSON操作
セキュリティhelmet, cors, rate-limiting, バリデーション
運用デバッグ、エラーハンドリング、ログ管理

Node.jsの理解が深まると、次のステップとしてデータベース接続(MongoDB, PostgreSQL)、REST API設計、認証(JWT)、デプロイ(Docker, クラウドサービス)へと進むことができる。

参考リンク