【Node.js実戦習得】第5回:package.jsonを解き明かす & モジュールシステムの歴史とモダン化への道

社員ブログ

1. package.jsonの役割を解剖!プロジェクトの初期化

酒本先輩、普段フロントのプロジェクトを触るとき、当たり前のようにフォルダの中に package.json がありましたけど、バックエンドのNode.jsにおける package.json って、何か役割が変わったりするんですか?

基本の概念は同じ。そのプロジェクトの名前、バージョン、設定、そしてインストールした『外部ライブラリのリスト』が記録された、いわば『プロジェクトの全知全能の設計図(取扱説明書)』ね。

ただ、フロントエンドだとViteやNext.jsのようなフレームワークが裏で隠蔽してくれている部分も、バックエンドでは自分たちで剥き出しのJSONをコントロールする必要があるの。

さっそく、いつもの nodejs-sample フォルダの中に、新しく実験用のディレクトリを作ってゼロから初期化してみましょう!

cd ~/nodejs-sample
mkdir node-npm-demo
cd node-npm-demo

# プロジェクトの初期化(すべてデフォルト設定で強制生成)
npm init -y

あ、おなじみの設定ファイルが生成されました!

中身はシンプルですね

{
  "name": "node-npm-demo",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}

そう、このJSONファイルがすべての土台になるの。

ここに書かれている項目について、実務でよく弄る部分を少し深掘りして解説しておくね。

ここをテキトウに流していると、チーム開発で設定がバッティングして痛い目を見るからね

💡 実務視点での各項目ディープ解説

  • name / version プロジェクトの識別名とバージョンよ。もし自作のライブラリとして世界(npmレジストリ)に公開するなら全世界でユニークにする必要があるけれど、社内のWebサーバーやAPIを構築するだけなら初期値のままで問題ない。ただし、バージョン表記は セマンティックバージョニング(メジャー.マイナー.パッチ) の規則に従うのがマナーね。
  • main このプロジェクトの「エントリポイント」を指定する場所。他のプログラムからこのフォルダ自体が呼び出されたときに、最初に実行されるファイルを指定するの。デフォルトは index.js になっているわね。
  • scripts ショートカットコマンドを登録する場所。複雑なバックエンドの起動コマンドや、テスト、リント(静的解析)を一元管理する、開発効率の要よ。

2. 外部ライブラリの導入と node_modules に課された「3つの鉄則」

外部のライブラリを入れるときは、バックエンドでも変わらず npm install ですよね?

その通り。

今回は、実務のバックエンド開発でも日付や時間の計算・操作で非常によく使われる軽量ライブラリ date-fns をインストールしてみようか

npm install date-fns

node_modules フォルダが出現しました!

そして、package.jsondependencies に自動的に date-fns とそのバージョンが記録されました!

  "dependencies": {
    "date-fns": "^4.4.0"
  }

これで『このプロジェクトを動かすには date-fns が必要』という履歴が刻まれたわけね。

……ところで熊木くん。

この新しくできた node_modules、まさかそのまま Git でコミットして GitHub にプッシュしようなんて思ってないわよね?

えっ!?

あ、いや……フロントのときは先輩が作ったリポジトリをクローンして使っていたので、最初から .gitignore に設定されていて意識してませんでした。

やっぱりそのまま上げちゃダメなんですか?

絶対にダメ

なぜ node_modules をGitに含めてはいけないのか、理由を3教えてあげる

🚨 node_modules を Git に入れてはいけない3つの理由

  1. ファイル数と容量が「異常」に肥大化する: node_modules の中は、あなたが直接入れたライブラリだけでなく、そのライブラリが依存している別のライブラリ……と芋づる式に何千・何万というファイルがダウンロードされるよ。これをGit管理に含めると、リポジトリの容量が大きくなりすぎて、クローンやプッシュの動作がめちゃくちゃ重くなるわ。
  2. 「環境依存」のバイナリが含まれるケースがある: ライブラリの中には、インストールしたその瞬間に、そのマシンのOS(Windows、Mac、Linux)やCPUアーキテクチャに合わせて、内部のC言語などのソースコードをコンパイルして動的実体を作るものがあるの。つまり、Windows環境で生成した node_modules をそのままGit経由でLinuxのサーバーに持っていっても、互換性がなくて絶対に動かないよ。
  3. そもそも共有する必要がない: これが最大の理由ね。

というわけで、フォルダのルートに .gitignore という名前のファイルを作って、必ずこの除外設定を書いておいてね

node_modules/

実体を Git に入れないと、本番サーバーや他のメンバーのパソコンにコードを共有したとき、アプリが動かなくなりませんか?

熊木くん、何のために package.json があると思っているの?

この設計図さえ Git で正確に共有しておけば、コードを受け取った人がそのフォルダで npm install と1行叩くだけで、設計図を元に全く同じ構成の node_modules がインターネットから一瞬で自動復元されるわ。重たい実体をわざわざGitで送る必要なんてどこにもないのよ

なるほど!

設計図(package.json)さえあれば、中身はいつでもどこでも完璧に再現できるから、巨大な実体は送らなくていいのですね……。

バックエンド開発における『環境の再現性』と、設計図管理の本当の重要性が、今ようやく理解できました!

 

3. npm scripts を使いこなす!サーバー起動コマンドの共通化

設計図の仕組みが分かったところで、実際にライブラリを使ったコードを動かしてみましょう。

まずはファイルを準備してね

touch index.js

この index.js に、ライブラリを使って現在時刻を分かりやすい表記に整形して出力するコードを書いてみて

// index.js
const { format } = require('date-fns');

const now = new Date();
const formattedDate = format(now, 'yyyy年MM月dd日 HH時mm分ss秒');

console.log('現在時刻:', formattedDate);

よし、これを node index.js で実行……。

わ、現在の日時が綺麗にフォーマットされて出力されました!

ライブラリの便利さはバックエンドでも健在ですね。

……でも先輩、実際の現場のサーバーを起動するときも、毎回こうやって node index.js って手動で叩くんですか?

そんなわけないでしょ!

実務だと、本番環境、ステージング環境、開発環境ごとに渡す環境変数(NODE_ENV=production など)を切り替えたり、起動オプションをたくさん付けたりするから、コマンドが2行分くらいに長くなるの。

それを全員が手動で打ち間違えずに叩くなんて不可能でしょ?

だから、さっき言った package.jsonscripts を使って、コマンドをエイリアス(別名)化するのよ。以下のように書き換えてみて

  "scripts": {
    "start": "node index.js"
  },

書き換えました!

これで……ターミナルから npm start と打つだけで、裏側で node index.js が呼び出されて起動できるようになるんですね!

npm start

大正解!

バックエンド開発の実務では、ここに『テストを全自動で実行するコマンド(npm test)』や『TypeScriptをコンパイルするビルドコマンド(npm run build)』、あとは『データベースのテーブル構造を自動更新するマイグレーションコマンド』なんかを何行も仕込んでいくの。

チーム全員が環境に左右されず、共通の短いコマンドで安全に作業できるようにするための、バックエンド開発における超重要なマナーよ

4. 2つのモジュールシステム:CommonJS(CJS)と ES Modules(ESM)の深い溝

npm scripts、めちゃくちゃ便利ですね!

開発がすごくスムーズになりそうです。

……でも酒本先輩、さっき僕が書いた index.js のコードで、1つだけずっと頭の中でモヤモヤしている部分があるんです

あら、どこかしら?

遠慮せずに言ってみなさい

外部ライブラリを読み込むときに、const { format } = require('date-fns'); って書きましたよね。

これ、僕が普段フロントエンド(ReactやVue)でコンポーネントを書くときは、いつも import { format } from 'date-fns'; って記述していたんです。

なぜNode.jsのバックエンド環境だと、わざわざ require という違う書き方を使うんですか?

あ、それね。

実は、現在のJavaScriptには、ファイルを分割して連携させる仕組み(モジュールシステム)が2種類、並行して存在しているのよ

  • CommonJS(CJS): require()module.exports を使う。Node.jsが誕生した初期から使われている、サーバーサイドJavaScriptの伝統的規格。
  • ES Modules(ESM): importexport を使う。JavaScriptの言語公式規格(ECMAScript)が、後から正式に定めたブラウザ共通の標準方式。

えっ!

じゃあ、WebブラウザやReactなどのモダンフロントエンドで僕が使っていた import は公式の新しい規格で、Node.jsの require は昔の古い独自規格ってことですか?

歴史の年表を見ると納得がいくわよ。

Node.jsが誕生した2009年当時、JavaScriptは『ブラウザの上でちょっとしたアニメーションを動かすための簡易言語』という位置付けだったの。

だから、プログラムを複数のファイルに分割して読み込む公式の仕様なんて存在しなかった。

でも、本格的なバックエンドサーバーを作るにはファイル分割が絶対に不可欠でしょ?

だからNode.jsの創始者たちは、独自に『CommonJS』という素晴らしいファイル読み込みの仕組みを発明して、サーバーサイドで大成功を収めたの

なるほど、Node.jsが先走って道を切り拓いたんですね

そうそう。

でも、そのずっと後(2015年)になって、ようやく言語の公式仕様として import / export(ES Modules)が策定された。

結果として、Node.jsの世界には『15年以上の膨大なシステムと資産を持つCommonJS』と『モダンで公式標準のES Modules』が、混在することになったの。

これが、フロントから来たエンジニアを苦しめる謎のエラーの根本原因ね

5. 【実戦】Node.js環境でモダンな「import / export」を解禁する方法

なるほど……。

歴史的な背景があるのですね。

でも、もし次の現場のコード規約が『これからはモダンなES Modules(import構文)で統一する』だった場合、Node.jsでそのまま import を書くとどうなるんですか?

Good Point。

Node.jsはデフォルト状態だと、すべての .js ファイルを『伝統的なCommonJS』として解釈しようとするの。

だから、何も設定せずに import を書くと、容赦なく Cannot use import statement outside a module(モジュールの外ではimportステートメントは使えないわよ)っていう有名なエラーを出して強制終了するのよ。

Node.jsで公式の import / export を正常に動作させるには、主に2つの実務的なアプローチがあるの

方法①:package.json に "type": "module" を指定する(現代のデファクトスタンダード)

これが最も推奨されるスタンダードな解決策よ。

プロジェクト全体をES Modules環境に切り替えるために、package.json の中にこの1行を書き足してごらんなさい

{
  "name": "node-npm-demo",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module", 
  "scripts": {
    "start": "node index.js"
  }
}

こう宣言することで、このフォルダ配下にあるすべての .js ファイルが、Node.jsによって自動的に『モダンなES Modules』として解釈されるようになるわ。

じゃあ、これに合わせて index.js の中身を、熊木くんがフロントで見慣れているはずの import 構文に書き換えて、再度実行してみて!

// index.js (ES Modules形式にアップデート)
import { format } from 'date-fns';

const now = new Date();
const formattedDate = format(now, 'yyyy年MM月dd日 HH時mm分ss秒');

console.log('現在時刻(ESM版で型安全に実行):', formattedDate);
npm start

おぉ!

今度はエラーにならずに、見慣れた import のコードで綺麗に起動しました!

画面の出力結果も完璧です。バックエンドなのにフロントと同じ書き方ができるの、すごくストレスフリーですね!

方法②:拡張子を .mjs にする(ピンポイント切り替え)

プロジェクト全体を一気に切り替えたくない、あるいは古いCommonJSのプロジェクトだけど、この1ファイルだけは新しい import で書きたい!というときは、ファイルの拡張子を index.js ではなく index.mjs(Modular JS)にするの。

そうすると、Node.jsはpackage.jsonの設定を無視して、そのファイル単体をES Modulesとして実行してくれるわ。

逆に、全体は "type": "module" だけど、特定のファイルだけrequire を使いたいときは .cjs(Common JS)という拡張子にするのよ。

実務で他人が書いたソースコードを見たときにあるかもしれないから、知識として覚えておいてね

第5回 まとめ

  • package.json: プロジェクトの心臓部(設計図)。外部ライブラリのバージョン管理や、複雑な起動コマンド(npm scripts)を集約する。
  • node_modulesの鉄則: 実体は超巨大かつ環境依存のバイナリを含むため、絶対に Git の管理に含めてはならない。必ず .gitignore に登録し、開発者各自のローカル環境で npm install を叩いて再現する。
  • 2つのモジュールシステム: 先駆者としての歴史を持つ require (CommonJS) と、後発ながらJavaScriptの公式標準となった import (ES Modules) が存在する。
  • モダン化への設定: 現代のNode.js開発の現場で import 構文を標準利用する場合、package.json"type": "module" を指定するのが実務のセオリー!

次回:第6回「Node.jsプロジェクトへのTypeScript導入とtsconfig.jsonの設定」に続く

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