Node.js + Express + TypeScript マニュアル(VS Code / 初心者向け)

1. Visual Studio Codeで新規プロジェクトを作成する方法

前提:Node.js(LTS推奨)がインストール済み。
Node.js は JavaScript を実行するランタイム(実行環境)。npm は Node.js 付属のパッケージ管理ツール。

1-1. VS Codeでフォルダーを開く

  1. 作業用フォルダーを作る(例:C:\work\my-api)。
  2. VS Code → FileOpen Folder... でそのフォルダーを開く。
  3. VS Code のターミナルを開く:TerminalNew Terminal

1-2. npmでプロジェクト初期化(package.json作成)

package.json は「依存ライブラリ」「実行スクリプト」「プロジェクト名」などを保持する設定ファイルです。

npm init -y

1-3. 必要パッケージのインストール

ここでは「Express + TypeScript をコンパイルして実行する」ための最小構成を作ります。

種類入れるもの理由
実行時依存
dependencies
express HTTPサーバー / ルーティングなどのWebフレームワーク
開発時依存
devDependencies
typescript, @types/node, @types/express, ts-node, nodemon TypeScriptコンパイラ、型定義、TSの実行補助、保存時の自動再起動
npm i express
npm i -D typescript ts-node nodemon @types/node @types/express
用語
型定義(@types/...):JavaScriptライブラリに「TypeScript用の型情報」を付けるための別パッケージ。
devDependencies:開発に必要だが、配布物(実行環境)には不要なことが多い依存。

1-4. TypeScript設定ファイル(tsconfig.json)作成

tsconfig.json は TypeScript コンパイラ(tsc)の設定ファイルです。

npx tsc --init

このマニュアルの推奨設定例(必要最低限 + 実用)

tsconfig.json


my-api/
├─ src/
├─ dist/
├─ package.json
├─ tsconfig.json   ← ここ
  
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "sourceMap": true
  },
  "include": ["src"]
}
module について(重要)
ここでは "module": "CommonJS" にしています。Node.js の設定(package.json の "type")や import/export の扱いに関係します。
後で ESM(ES Modules)に寄せることもできますが、初心者向けの混乱を避けるためまずは CommonJS を採用しています。

1-5. エントリーファイルを作る

src フォルダーを作成し、src/index.ts を作ります。

import express, { Request, Response } from "express";

const app = express();
app.use(express.json()); // JSON のリクエストボディを読むため

app.get("/health", (req: Request, res: Response) => {
  res.json({ ok: true, time: new Date().toISOString() });
});

const port = Number(process.env.PORT ?? 3000);
app.listen(port, () => {
  console.log(`[server] listening on http://localhost:${port}`);
});
💡 実行時に決まる ビルドして zip 配布しても、
PORT の値はまだ決まっていません。
PORT=8080 npm start
このコマンドを実行したその瞬間に、
process.env.PORT の値が読み取られ、ポート番号が決まります。

1-6. package.json にスクリプトを追加

npm scripts は「よく使うコマンド」を短い名前で登録する仕組みです。

{
  "name": "my-api",
  "version": "1.0.0",
  "main": "dist/index.js",
  "scripts": {
    "dev": "nodemon --watch src --ext ts --exec ts-node src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
  },
  "dependencies": {
    "express": "^4.19.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.0",
    "@types/node": "^20.0.0",
    "nodemon": "^3.0.0",
    "ts-node": "^10.9.0",
    "typescript": "^5.0.0"
  }
}
用語
npx:インストール済みパッケージのコマンドを「パス設定なしで」実行する仕組み。
トランスパイル:TypeScript を JavaScript に変換すること(コンパイルと同義扱いでOK)。
sourceMap:デバッグ時に「変換前のTS行番号」に戻せる情報。

2. 一般的なフォルダー構成

小〜中規模の API サーバーでよく見る、理解しやすい構成例です。

my-api/
  ├─ src/                # TypeScriptのソース
  │   ├─ index.ts        # エントリ(起動点)
  │   ├─ app.ts          # Expressの設定(任意で分離)
  │   ├─ routes/         # ルーティング(URL→処理の割当)
  │   ├─ controllers/    # HTTPの入出力を扱う層(任意)
  │   ├─ services/       # 業務ロジック(任意)
  │   ├─ repositories/   # DBアクセス(任意)
  │   ├─ middlewares/    # 認証など共通処理(任意)
  │   └─ types/          # 自前の型定義(任意)
  ├─ dist/               # build(tsc)で生成されるJS(配布物)
  ├─ package.json
  ├─ package-lock.json   # 依存の固定(自動生成)
  ├─ tsconfig.json
  └─ node_modules/       # 依存(自動生成・通常Git管理しない)

役割の考え方(初心者が迷いやすいポイント)

この構成は「正解」ではなく「読みやすさ重視の定番」です。規模が小さい間は src/index.ts に寄せてもOKです。

3. コンパイル、実行方法

3-1. 開発モード(保存したら自動再起動)

ts-node で TypeScript を直接実行し、nodemon で変更検知して再起動します。

npm run dev

動作確認(ブラウザ or curl)

http://localhost:3000/health
ポイント
開発中は “速さと楽さ” が大事なので、dist を作らずに動かします(ts-node)。
本番は “確実さと再現性” が大事なので、dist を作って node で動かします(次項)。

3-2. ビルド(TypeScript → JavaScript)

npm run build

dist/index.js が作成されます。

3-3. 実行(生成されたJavaScriptをNodeで動かす)

npm start

3-4. よくあるエラーと原因

症状原因の典型対処
Cannot find module ... dist を消した / buildしていない / mainが違う npm run buildmaindist/index.js に合わせる
TypeError: (0 , express_1.default) is not a function import設定(Interop)が合っていない tsconfig.jsonesModuleInterop: true を確認
型エラーでビルドできない strict が効いている 型注釈・ガードを追加(後述)

4. リリースビルド、配布

4-1. “配布物” とは何か

Node.js アプリは「単一exeに固めて配布」よりも、通常は以下の形になります。

重要:実行環境(配布先)には Node.js が必要です。
「Node.js を同梱して単体配布」もできますが(pkg/nexe 等)、まずは標準的な配布から始めるのが学習コストが低いです。

4-2. リリース手順(標準)

  1. 開発PCでビルド
    npm run clean
    npm run build
  2. 配布先フォルダーへファイルをコピー(例)
    dist/
    package.json
    package-lock.json
  3. 配布先で依存をインストール
    npm ci --omit=dev
    npm ci:lockファイル(package-lock.json)どおりに機械的にインストールするコマンド。再現性が高い。
    --omit=dev:devDependenciesを入れない(本番に不要なものを省く)。
  4. 起動
    npm start

4-3. 環境変数でポートなどを切り替える

環境変数 は、コードを書き換えずに設定を変える仕組みです(例:ポート、DB接続先、APIキー)。

# Windows PowerShell
$env:PORT="8080"
npm start

# macOS / Linux
PORT=8080 npm start

4-4. 配布の形の例

この章では “標準の配布” までに絞っています。
「Windowsサービス化」「PM2」「Docker」などは次の章として分けた方が読みやすいです。

5. TypeScriptの型、文法(if, switch, while, for, foreach など)

前提:TypeScript は “JavaScript + 型” です。実行時には型は消えます(型は開発時の安全装置)。
つまり「型で事故を減らす」ために書きます。

5-1. 基本の型

意味
stringconst s: string = "abc"文字列
numberconst n: number = 123数値(整数/小数の区別なし)
booleanconst b: boolean = truetrue/false
null / undefinedlet x: string | null = null値がない状態(2種類)
anylet v: any = ...型チェック放棄(最後の手段)
unknownlet v: unknown不明な値(使う前に絞り込みが必要)
voidfunction f(): void {}戻り値を返さない
neverfunction die(): never { throw ... }絶対に正常終了しない

5-2. 配列・タプル

const nums: number[] = [1, 2, 3];
const names: Array<string> = ["a", "b"];

// タプル(要素数と各要素の型が固定)
const pair: [number, string] = [1, "one"];

5-3. オブジェクト型(interface / type)

API開発では最重要です(リクエストやレスポンスの形を型で固定できる)。

// interface:主に「形」を定義する(拡張もしやすい)
interface User {
  id: number;
  name: string;
  email?: string;  // ?: あってもなくてもよい(optional)
}

// type:unionなど「型の合成」に強い
type Id = number | string;

const u: User = { id: 1, name: "mao" };
📘 interface の本質 TypeScript の interface は、型チェックのためだけに使われます。
実行時には存在せず、new されたり、メモリを持ったりすることはありません。

interface の役割
  • オブジェクトの「形」を定義する
  • 代入・引数・戻り値の型チェックを行う
  • API の入出力仕様を明確にする(生きた仕様書)
  • VS Code の補完・エラー表示を強化する
重要なポイント
interface はクラスの設計図ではなく、
「この形を満たしているか」をコンパイル時に検査するためのルールです。
interface User {
  id: number;
  name: string;
}

const u: User = { id: 1, name: "mao" }; // ← ただのJSオブジェクト
上記のコードでも、User は実行時には存在しません。
📘 TypeScript の class / enum / abstract class を順に理解する 最初に結論
TypeScript では classenumabstract class(抽象クラス)すべて使うことができます。 ただし、それぞれは 役割がまったく異なる ため、 使い分けがとても重要です。
① class:実体を持つ「オブジェクト」
class は Java / C# とほぼ同じ感覚で使えます。
class User {
  constructor(
    public id: number,
    public name: string
  ) {}

  greet(): string {
    return `hello ${this.name}`;
  }
}

const u = new User(1, "mao");
u.greet();
class の特徴は次の通りです。
  • new してインスタンスを作る
  • 実行時に存在する
  • 状態(フィールド)と振る舞い(メソッド)を持つ
👉 「処理」「ロジック」「サービス」 を表したいときに使います。
② enum:状態・種別を表すための定数集合
enum は「取り得る値が決まっているもの」を表すために使います。
enum Status {
  Pending,
  Paid,
  Canceled
}

const s: Status = Status.Paid;
これは実行時にオブジェクトとして残ります。
// 実行時にも存在する
console.log(Status.Paid); // 1
文字列 enum(実務ではこちらが推奨)
enum Role {
  Admin = "admin",
  User = "user"
}
👉 API や DB と連携する場合、 数値 enum より文字列 enum の方が安全です。
③ const enum:enum の軽量版(注意)
const enum Mode {
  Dev,
  Prod
}
これはビルド時に数値へ置き換えられます。
// Mode.Dev → 0 に変換される
👉 実行時には存在しません。 設定によってはトラブルになるため、初心者は無理に使わなくてOKです。
④ abstract class:継承専用のクラス
抽象クラスは「そのまま new されることを想定しない class」です。
abstract class Repository {
  abstract findById(id: number): unknown;

  log(msg: string) {
    console.log(msg);
  }
}
抽象クラスの特徴:
  • new できない
  • 継承されることが前提
  • 共通処理を持てる
  • 実装すべきメソッドを強制できる
継承例
class UserRepository extends Repository {
  findById(id: number) {
    return { id, name: "mao" };
  }
}
👉 「共通の枠組み + 実装の強制」をしたいときに使います。
⑤ interface との違い(ここが混乱ポイント)
interface User {
  id: number;
  name: string;
}
interface は:
  • 実行時に存在しない
  • new できない
  • 型チェック専用
👉 「データの形」を表すためのものです。
⑥ 実務での使い分け(超重要)
用途使うもの
APIの入出力・DTOinterface / type
状態・種別enum(文字列)
処理・サービスclass
共通基底クラスabstract class
典型的な実務例
interface UserDto {
  id: number;
  name: string;
}

class UserService {
  create(user: UserDto) {
    // 処理を書く
  }
}

最終まとめ
TypeScript では:
  • class:実体とロジック
  • enum:状態・種別
  • abstract class:共通ルールと枠組み
  • interface:データの形(比較対象)
レイヤーごとに使い分ける のが正解です。

「全部 class で書く」「全部 interface で書く」ではなく、 役割で選ぶことで、読みやすく壊れにくい設計になります。
📘 TypeScript に「構造体(struct)」はあるのか? 最初に結論
TypeScript には、C / C++ / C# にあるような 「struct」という専用の言語機能はありません。

ただしこれは「できない」という意味ではなく、 TypeScript では struct と同じ目的を、別の書き方で実現する という設計になっています。
そもそも「struct が欲しい」とは、何をしたいのか?
初心者が「struct はないのですか?」と感じる場面は、だいたい次の用途です。
  • 複数の値を 1 つのまとまりとして扱いたい
  • クラスほど重い仕組みは使いたくない
  • メソッドやロジックは不要
  • API や DB のデータ構造を表したい
これを踏まえて、TypeScript での書き方を 1 つずつ見ていきます。
① interface:もっとも基本的な「struct 相当」
// データの「形」だけを定義
interface User {
  id: number;
  name: string;
  email?: string; // あってもなくてもよい
}

// 普通の JavaScript オブジェクト
const u: User = {
  id: 1,
  name: "mao"
};
この User は次の特徴を持ちます。
  • 実行時には存在しない
  • new できない
  • メモリを持たない
  • 型チェックのためだけに使われる
👉 これは 「軽量な構造体」と考えて問題ありません。 API の入出力(DTO)では、最もよく使われます。
② type:より柔軟な構造体
interface と似ていますが、型の合成が得意です。
type Point = {
  x: number;
  y: number;
};

const p: Point = { x: 10, y: 20 };
union(どれか1つ)も書けます。
type Id = number | string;

function find(id: Id) {
  // id は number か string
}
👉 「struct + バリエーション」が欲しいときは type を使います。
③ readonly:変更できない構造体(値オブジェクト)
type Point = {
  readonly x: number;
  readonly y: number;
};

const p: Point = { x: 10, y: 20 };
// p.x = 30; ❌ コンパイルエラー
一度作ったら変更できないため、
  • 座標
  • 金額
  • 設定値
などに向いています。 👉 C# の readonly struct に近い考え方です。
④ as const:定数としての構造体
const Status = {
  Pending: "pending",
  Paid: "paid",
  Canceled: "canceled"
} as const;
これは:
  • 実行時は普通のオブジェクト
  • 型的には値が固定される
👉 enum の代替として、実務でよく使われます。
⑤ class:これは「構造体」ではない
class User {
  constructor(
    public id: number,
    public name: string
  ) {}

  greet() {
    return `hello ${this.name}`;
  }
}

const u = new User(1, "mao");
class は:
  • 実行時に存在する
  • new される
  • メソッド(振る舞い)を持つ
👉 「データだけ」を表したい場合、class は重すぎることが多いです。
⑥ 実務での定番パターン
// 構造体相当(API / DB / DTO)
interface UserDto {
  id: number;
  name: string;
}

// 処理・ロジック担当
class UserService {
  create(user: UserDto) {
    // user はただのデータ
  }
}
実務ではこのように:
  • データ構造 → interface / type
  • 処理・振る舞い → class
と分けるのが最も一般的です。
比較まとめ
書き方実体用途
interfaceなしデータ構造(struct相当)
typeなし柔軟な構造体
readonlyなし不変な値
as constあり定数集合
classあり状態+振る舞い
最終まとめ
TypeScript に struct はありません。 しかし、
  • 「データだけ」→ interface / type
  • 「変更不可」→ readonly
  • 「定数」→ as const
  • 「振る舞い」→ class
と考えることで、 struct が欲しくなる場面はすべて自然に解決できます。

5-4. Union(合併型)と型ガード(絞り込み)

type Input = string | number;

function toNumber(x: Input): number {
  if (typeof x === "string") {
    return Number(x);
  }
  return x; // ここでは number と分かっている
}
unknown とセットで覚えると強い
外部入力(HTTP body など)は基本 “信用しない” → unknown で受け → 条件で絞る、が堅い書き方です。
📘 Union(合併型)と型ガードとは何か この章で扱っている問題
JavaScript では、関数に渡される値の「型」が実行時まで分かりません。 そのため、次のようなコードは普通に書けてしまいます。
function addOne(x) {
  return x + 1;
}

addOne("10"); // "101"
addOne(10);   // 11
この関数は「数値を想定している」ように見えますが、 文字列が渡されてもエラーになりません。 これは JavaScript の仕様ですが、実務ではバグの原因になります。
Union(合併型)とは何か
Union 型とは、
「この値は A か B のどちらかである」
ということを 型として明示する仕組みです。
type Input = string | number;
この型は、
  • string かもしれない
  • number かもしれない
  • それ以外ではない
という意味を持ちます。
Union 型のままでは、なぜ使えないのか
type Input = string | number;

function f(x: Input) {
  return x + 1; // ❌ コンパイルエラー
}
TypeScript はこのコードを見て、次のように考えます。
  • x が string の場合 → "10" + 1 → "101"
  • x が number の場合 → 10 + 1 → 11
結果が2通りあり、どちらになるか分からないため、 TypeScript は「安全ではない」と判断してエラーにします。
型ガード(型の絞り込み)とは何か
型ガードとは、
Union 型の値について 「今この場所では、どの型なのか」を確定させる処理
のことです。 一番基本的な型ガードは typeof です。
typeof x === "string"
これは:
  • 実行時には JavaScript の条件判定
  • コンパイル時には TypeScript へのヒント
という 2つの意味を持ちます。
型ガードがあると、何が変わるのか
function f(x: string | number) {
  if (typeof x === "string") {
    // この中では x は string
    x.toUpperCase(); // OK
  } else {
    // この中では x は number
    x.toFixed(2); // OK
  }
}
if 文の中で条件をチェックすることで、 TypeScript は x の型を段階的に絞り込みます。 これを 型の絞り込み(narrowing) と呼びます。
質問のコードを、1行ずつ読む
type Input = string | number;

function toNumber(x: Input): number {
  if (typeof x === "string") {
    return Number(x);
  }
  return x; // ここでは number と分かっている
}
① Union 型の定義
Input は「string または number」を表します。

② 関数の目的
この関数は、 string が来ても number が来ても、必ず number を返す ことを目的としています。

③ if 文による型ガード
typeof x === "string" が true の場合、 このブロック内では x は string だと確定します。

④ string の場合の処理
文字列はそのまま返せないため、 Number(x) で数値に変換しています。

⑤ else 側の return
if 文で string の可能性を除外しているため、 この地点では x は number しかありえません。 そのため、そのまま return できます。
この書き方は、どんな場面で使われるのか
実務では、次のような場面で必ず使われます。
  • HTTP リクエストの body
  • URL クエリパラメータ
  • フォーム入力値
  • JSON を parse した結果
これらはすべて 「外部から来る=信用できない値」です。
unknown と組み合わせる理由
function parse(v: unknown): number {
  if (typeof v === "number") {
    return v;
  }
  if (typeof v === "string") {
    return Number(v);
  }
  throw new Error("invalid input");
}
unknown は、 「何が来るか本当に分からない」ことを表します。 そのため、
  • いきなり使うことはできない
  • 必ず型ガードが必要
という制約があります。 👉 これにより、 危険な値を安全に処理する強制力が生まれます。
この考え方の本質
Union 型と型ガードは、
  • 「受け取りは柔軟に」
  • 「内部では厳密に」
という設計を可能にします。

外部入力を扱う TypeScript のコードでは、 必ず登場する基本パターンです。

5-5. if / switch

const status = "paid";

if (status === "paid") {
  // ...
} else if (status === "pending") {
  // ...
} else {
  // ...
}

switch (status) {
  case "paid":
    // ...
    break;
  case "pending":
    // ...
    break;
  default:
    // ...
    break;
}

5-6. while / for

let i = 0;
while (i < 3) {
  console.log(i);
  i++;
}

for (let j = 0; j < 3; j++) {
  console.log(j);
}

5-7. “foreach” に相当するもの(for...of / forEach)

TypeScript に foreach キーワードはありません。代わりに以下を使います。

const arr = ["a", "b", "c"];

// 1) for...of(おすすめ:break/continueが使える)
for (const x of arr) {
  console.log(x);
}

// 2) Array.forEach(関数で回す。breakできない)
arr.forEach((x) => {
  console.log(x);
});
forEach の注意
途中で抜けたい(break)時に困りやすいので、迷ったら for...of が安全です。

5-8. 関数・戻り値・型注釈

// 引数と戻り値に型を付ける
function add(a: number, b: number): number {
  return a + b;
}

// アロー関数(よく使う)
const add2 = (a: number, b: number): number => a + b;

5-9. 例外(throw)と try/catch と型

JavaScript/TypeScript には 例外(Exception) があり、throw で発生させ、 try/catch で捕まえます。
TypeScript で増えるポイントは「catch で受け取る値の型がどう扱われるか」です。

📘 用語 throw:例外を発生させて、その場で処理を中断する。
try/catch:例外が起きうる処理を囲み、例外が起きたら catch 側で処理する。
finally:成功・失敗に関わらず必ず実行する後片付けブロック(任意)。

1) throw の基本(不正な値を拒否する)

function mustBePositive(n: number): number {
  if (n <= 0) throw new Error("n must be positive");
  return n;
}

// 呼び出し側が catch しないと、例外は呼び出し元へ伝播していく
💡 何をしているコード? n が 0 以下なら「この関数は続行できない」と判断して例外を投げ、
正の数ならそのまま返します。
つまり 入力チェック(ガード)として使っています。

2) try/catch の基本(投げられた例外を処理する)

try {
  const x = mustBePositive(-3);
  console.log("OK:", x);
} catch (e) {
  console.log("NG: must be positive");
}
💡 try/catch はいつ使う? 例外が起きうる処理(入力チェック、JSON.parse、外部API呼び出しなど)を囲み、
「失敗したときにどうするか」を catch に書きます。

3) TypeScript的に重要:catch の e は何型?

JavaScript では throw できる値は Error とは限らず、 文字列やオブジェクトなど何でも投げられます。
そのため TypeScript では catch の変数 e は基本的に unknown 扱いになります(安全側)。

try {
  throw new Error("boom");
} catch (e) {
  // e は unknown だと考える(いきなり e.message は使えない)
  if (e instanceof Error) {
    console.log(e.message); // OK(ここで Error に絞り込めた)
  } else {
    console.log("Unknown error");
  }
}
⚠ 注意 catch の e を「必ず Error」と決めつけるのは危険です。
実務では e instanceof Error などの 型ガードで絞り込んでから扱うのが安全です。

4) finally(必ず実行したい後処理)

try {
  // 例:ファイル/通信/ロックなど、後で必ず解放したい処理
  console.log("start");
  const x = mustBePositive(10);
  console.log("value:", x);
} catch (e) {
  console.log("error");
} finally {
  console.log("cleanup"); // 成功でも失敗でも必ず実行
}

5) 実務での典型:HTTP API(Express)ではどうする?

Node.js/Express では「例外を投げる」よりも、
HTTP ステータスと JSON を返してエラーを表現するのが一般的です。
ただし、入力チェックなどで throw して、上位でまとめて catch する設計もあります。

// 例:入力チェックをして、ダメなら 400 を返す(throw を使わない)
app.post("/items", (req, res) => {
  const n = Number(req.body.amount);
  if (!Number.isFinite(n) || n <= 0) {
    return res.status(400).json({ error: "amount must be positive" });
  }
  res.json({ ok: true });
});
✅ 使い分けの目安 throw を使う:関数内部で「これ以上続行不能」を表したい(入力ガード、到達不能など)
HTTPで返す:APIとして「失敗をレスポンスで表現」したい(400/404/500 など)

5-10. 非同期処理(Promise / async / await)— Node.jsで最重要

Node.js では、HTTP・ファイル・DB などの I/O(入出力)は 非同期 が基本です。
理由は単純で、I/O は「待ち時間(通信やディスク待ち)」が長く、そこで処理を止めると 1つのプロセスが何もできない時間が増え、同時にさばけるリクエスト数が減ってしまうからです。

📘 用語(この節で必要なものだけ) 同期(synchronous):処理が終わるまで次へ進まない。
非同期(asynchronous):処理を開始して「結果はあとで受け取る」。待ち時間に他の処理を進められる。
Promise:未来の結果(成功なら値、失敗ならエラー)を表す「箱」。
await:Promise の完了を待って、成功なら値を受け取る。失敗なら例外として投げ直される。
async:その関数は必ず Promise を返す、という宣言(戻り値が自動で Promise に包まれる)。

1) まず「同期」と「非同期」の違いを体感する

I/O を同期で書くと(概念的には)こうなります:

// (説明用の擬似コード)同期処理のイメージ
// request を送る → 応答が来るまで止まる → 受け取ったら次へ
const data = httpGetSync("https://example.com"); // 終わるまで待つ
console.log("data:", data);
console.log("次の処理");

これだと待っている間、CPU がヒマになります。
Node.js は「待ち時間に別の処理を進める」設計なので、I/O は非同期が基本です。

// 非同期処理のイメージ(Promise)
const p = httpGetAsync("https://example.com"); // すぐ返る(結果は未来)
console.log("次の処理(待たずに進む)");

// あとで結果を受け取る
p.then((data) => console.log("data:", data))
 .catch((err) => console.error("error:", err));
💡 ここが重要 非同期では「結果がまだない」のに関数が戻るため、
data の代わりに Promise(未来の結果)を受け取る必要があります。

2) Promise とは何か(Android Java経験者向けの対応づけ)

Promise は「将来完了する処理」を表すオブジェクトです。
Android Java で例えると、ざっくり次のどれかに近いです(完全一致ではありません)。

ただし Node.js では、こうした非同期が「標準の書き方」として組み込まれており、 ファイル・HTTP・DB などが最初から Promise を返す設計が多いです。


3) Promise の基本:then/catch(素の書き方)

function fetchTextThenStyle(): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    setTimeout(() => {
      const ok = true; // 失敗させたいなら false にする
      if (ok) resolve("hello");
      else reject(new Error("failed"));
    }, 100);
  });
}

fetchTextThenStyle()
  .then((text) => {
    console.log("success:", text);
  })
  .catch((err) => {
    console.error("error:", err.message);
  });
📘 resolve / reject とは resolve(value):成功として Promise を完了させ、値を返す。
reject(error):失敗として Promise を完了させ、エラーを返す。
(Android の callback で言うと onSuccess / onError のような役割)

4) async / await:Promise を「同期っぽく」書く糖衣構文

then/catch は慣れると便利ですが、処理が長くなると読みにくくなりがちです。
そこで async/await を使うと、同期処理に近い形で書けます。

// Promise を返す関数(上の例と同じことをする)
function fetchText(): Promise<string> {
  return new Promise<string>((resolve) => {
    setTimeout(() => resolve("hello"), 100);
  });
}

// async/await で「待って値を受け取る」ように見せる
async function main(): Promise<void> {
  const text = await fetchText(); // ここで Promise の完了を待つ
  console.log("text:", text);
}

main();
💡 async / await のルール(最重要)
  • awaitasync 関数の中でしか使えない
  • async function必ず Promise を返す
  • return 123; と書いても、戻り値は Promise<number> になる

5) 例外(throw)と await(ここで try/catch が重要になる)

Promise が失敗(reject)したとき、await はその失敗を 例外として投げます
つまり await は throw を発生させうるので、必要なら try/catch で捕まえます。

async function mayFail(): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error("network error")), 100);
  });
}

async function run(): Promise<void> {
  try {
    const v = await mayFail(); // reject されたらここで例外になる
    console.log("OK:", v);
  } catch (e) {
    // Java と同じく「失敗時の処理」をここに書く
    if (e instanceof Error) console.log("NG:", e.message);
    else console.log("NG: unknown error");
  }
}

run();
⚠ よくある勘違い await は「失敗を返す」のではなく、
失敗(reject)を例外(throw)として投げる、と覚えると混乱が減ります。

6) 実務:HTTP(fetch)— 外部APIを呼ぶ典型

Node.js では HTTP 呼び出しは非同期が基本です。
(Node のバージョンや環境によっては fetch を使います。ここでは概念が目的です。)

// 概念例:HTTP で JSON を取る(Promise → await)
async function fetchJson(url: string): Promise<unknown> {
  const res = await fetch(url);              // HTTP待ち
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return await res.json();                   // JSON変換も非同期
}

返ってくる JSON は信用できないため、unknown として受け、 型ガードで絞る(5-4 の内容)に繋がります。


7) 実務:ファイル(fs/promises)— ファイルI/Oも await

// 概念例:ファイル読み込み(Nodeでは非同期が基本)
import { readFile } from "node:fs/promises";

async function loadText(path: string): Promise<string> {
  const buf = await readFile(path);   // ディスク待ち
  return buf.toString("utf-8");
}

8) 実務:並列処理(Promise.all)— 待ち時間を短くする

非同期は「順番に await する」だけでなく、同時に走らせることも重要です。
例えば 3 つの HTTP を順番に待つと遅くなります。

// 悪い例:順番に await(合計時間が長くなる)
const a = await fetchJson(urlA);
const b = await fetchJson(urlB);
const c = await fetchJson(urlC);

// 良い例:同時に開始して、全部終わるのを待つ
const [aa, bb, cc] = await Promise.all([
  fetchJson(urlA),
  fetchJson(urlB),
  fetchJson(urlC),
]);
💡 実務のコツ 「A の結果を使って B を呼ぶ」など依存がある場合は順番に await。
依存がない場合は Promise.all で並列化すると速くなります。

9) Android Java と比べて何が違うのか(感覚を揃える)


10) ありがちな落とし穴(初心者が最初に踏む)

✅ この節のゴール Node.js の I/O は非同期が基本で、戻り値は Promise<T> になる。
async/await を使うと、同期処理に近い形で安全に書ける。
失敗(reject)は await で例外になり、必要なら try/catch で扱う。

サンプル:/user/get/all → 他サーバーGET → JSON解析 → 必要項目だけ返す

この節でやること(概要):

  1. Express の RouterGET /user/get/all を受ける
  2. Router から Controller を呼ぶ
  3. Controller の中で「他サーバーへ HTTP GET」して JSON を受け取る
  4. 受け取った JSON の中から userid, username, email だけを抜き出す
  5. クライアントへ id,name,email の一覧を JSON で返す
  6. 同じ処理を async/await 版と .then().catch() 版で見比べる
⚠ 他サーバーのレスポンスJSONについて 次のような JSON を受け取る想定で説明します。
{
  "users": [
    { "userid": 1, "username": "john", "email": "john@company.com", "tel": "080-1721-0000", "nickname": "yobochan" },
    { "userid": 2, "username": "alice", "email": "alice@company.com", "tel": "090-0000-1111", "nickname": "ali" }
  ]
}

まず「全体コード」を先に見せます(async/await 版)

フォルダ構成(例):

src/
  app.ts
  routes/
    userRoutes.ts
  controllers/
    userController.ts
  services/
    externalUserService.ts
  types/
    externalUserTypes.ts
src/types/externalUserTypes.ts(型定義:受け取るJSONと返すJSON)
export interface ExternalUser {
  userid: number;
  username: string;
  email: string;
  tel: string;
  nickname: string;
}

export interface ExternalUsersResponse {
  users: ExternalUser[];
}

export interface PublicUser {
  id: number;
  name: string;
  email: string;
}
src/services/externalUserService.ts(他サーバーへHTTP GETしてJSONを返す)
import type { ExternalUsersResponse } from "../types/externalUserTypes";

// NOTE: Node 18+ なら fetch が標準で使えることが多いです。
// もし fetch が使えない環境なら、node-fetch を入れる/axios を使う等に置き換えます。
export async function fetchExternalUsers(baseUrl: string): Promise<ExternalUsersResponse> {
  const url = `${baseUrl}/api/users`; // ← 他サーバーのAPIパス(例)
  const res = await fetch(url, { method: "GET" });

  if (!res.ok) {
    // await では reject が「例外」になるので、ここで throw すると caller が catch できます
    throw new Error(`External API failed. status=${res.status}`);
  }

  // ここでは「外部入力は信用しない」ので、本当は unknown で受けて検証したい。
  // まずは学習用に、型アサーションで簡略化します。
  const data = (await res.json()) as ExternalUsersResponse;

  // 最低限の形チェック(雑にでもやると安全度が上がります)
  if (!data || !Array.isArray(data.users)) {
    throw new Error("External API JSON shape is invalid (users not found).");
  }

  return data;
}
src/controllers/userController.ts(受け取ったJSONを解析し、必要項目だけ返す)
import type { Request, Response, NextFunction } from "express";
import { fetchExternalUsers } from "../services/externalUserService";
import type { PublicUser } from "../types/externalUserTypes";

const EXTERNAL_BASE_URL = process.env.EXTERNAL_BASE_URL ?? "https://other.example.com";

export async function getAllUsers(req: Request, res: Response, next: NextFunction) {
  try {
    // 1) 他サーバーへ GET
    const external = await fetchExternalUsers(EXTERNAL_BASE_URL);

    // 2) 必要項目だけに変換
    const list: PublicUser[] = external.users.map(u => ({
      id: u.userid,
      name: u.username,
      email: u.email,
    }));

    // 3) 自サーバーのAPIレスポンスとして返す
    res.json({ users: list });

  } catch (err) {
    // Controller 内で catch して HTTP 500/502 等で返しても良いし、
    // next(err) で「エラーハンドリングミドルウェア」に任せても良いです。
    next(err);
  }
}
src/routes/userRoutes.ts(Router:/user/get/all を Controller に接続)
import { Router } from "express";
import { getAllUsers } from "../controllers/userController";

export const userRouter = Router();

// router で /user/get/all を受ける想定
userRouter.get("/get/all", getAllUsers);
📘 なぜ "/get/all""/user/get/all" になるのか

この行を見て、こう疑問に思ったはずです:

userRouter.get("/get/all", getAllUsers);

コード上は /get/all なのに、なぜ実際のURLは /user/get/all になるのか?


結論を先に
Express では、URL は次の 2 つを 連結 して作られます。
  1. app.use() で指定した「ベースパス」
  2. Router 内で定義したパス
つまり:
app.use("/user", userRouter)

userRouter.get("/get/all", ...)

/user/get/all

① app.ts 側の設定(入口)
app.use("/user", userRouter);

これは次の意味です:

/user で始まるリクエストは、
すべて userRouter に渡す」

この時点で、Router に渡される時のURLから /user は削られます。


② Router 側の定義(中身)
userRouter.get("/get/all", getAllUsers);

ここで定義しているのは:

「Router に渡ってきた URL のうち、
/get/all に一致するもの」

③ 実際のリクエストの流れ(時系列)

クライアントが次のURLにアクセスしたとします:

GET /user/get/all

Express 内部では、次の順で処理されます:

  1. app レベル
    「URL が /user で始まっているか?」
    → YES → userRouter に処理を委譲
  2. Router レベル
    Router に渡る時点で URL は /get/all になる
  3. Router 内の定義と照合
    "/get/all" に一致 → getAllUsers が呼ばれる

図でイメージすると
ブラウザ / クライアント
        |
        | GET /user/get/all
        v
+-------------------+
| app.use("/user")  |  ← /user を剥がす
+-------------------+
        |
        | /get/all
        v
+-------------------+
| userRouter        |
| .get("/get/all")  |
+-------------------+
        |
        v
  getAllUsers()

もし Router 側でこう書いたら?
userRouter.get("/", handler);
この場合のURLは:
GET /user

つまり Router 内のパスは、 「app.use で切られた後の残り」を書く、という感覚です。


よくある勘違い
  • ❌ Router の "/get/all" が絶対URLだと思う
    → 実際は 相対パス
  • ❌ Router は独立したサーバーだと思う
    → 実際は app の下にぶら下がる部品

Android(例:Spring MVC)と対応づけると
@RequestMapping("/user")
public class UserController {

  @GetMapping("/get/all")
  public Response getAll() { ... }
}

これと まったく同じ構造です。

  • @RequestMapping("/user")app.use("/user", ...)
  • @GetMapping("/get/all")router.get("/get/all")

この設計のメリット
  • URL 構造をフォルダ単位で整理できる
  • /user, /admin, /product を分離しやすい
  • Router をテスト・再利用しやすい

まとめ(ただし短くしすぎない)
Router に書くパスは、 「app.use() で指定したパスの続き」を書く。
だから:
app.use("/user", userRouter)

userRouter.get("/get/all")

/user/get/all
この理解ができると、Express の URL 設計で迷わなくなります。
src/app.ts(Expressアプリ:Router登録とエラーハンドリング)
import express from "express";
import { userRouter } from "./routes/userRoutes";

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

// /user/... を userRouter に委譲
app.use("/user", userRouter);

// エラーハンドリングミドルウェア(next(err) が来たらここに来る)
app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
  // 実務ではログ出しや err の種類判定(外部API失敗=502など)を行います
  const message = err instanceof Error ? err.message : "Unknown error";
  res.status(500).json({ error: message });
});

const port = Number(process.env.PORT ?? 3000);
app.listen(port, () => {
  console.log(`[server] listening on http://localhost:${port}`);
});
✅ async/await 版の流れ(最短で理解するポイント) Router → Controller → (await) 外部HTTP → JSON → mapで抽出 → res.json で返す → 失敗は try/catch で next(err)。

📘 await fetch(url) は「何を待っている」のか 質問のコード
const res = await fetch(url, { method: "GET" });
この await は、
HTTP 通信が「完全に終わるまで」待っているわけではありません。
結論から正確に言うと
await fetch(...) が待っているのは、
HTTP リクエストを送信し、
サーバーから レスポンスヘッダ(ステータスコードやヘッダ情報) が返ってくるまで
です。

レスポンスの本文(JSONやテキスト)は、まだ受信し終わっていません。
HTTP 通信は内部的に 2 段階で進む
  1. リクエスト送信 → レスポンスヘッダ受信
  2. レスポンス本文(ボディ)を最後まで読む
Node.js の fetch は、この 2 段階を明確に分けています。
① fetch が返すもの
const res = await fetch(url);
この時点で res に入っているのは:
  • HTTP ステータス(200 / 404 / 500 など)
  • レスポンスヘッダ
  • レスポンス本文を読むためのハンドル
つまり、
「サーバーが返事をし始めた」ことが分かった状態
です。

本文(JSON やテキスト)は、まだ読み終わっていません。
② レスポンス本文を読むのは別の await
const json = await res.json();
ここで初めて:
  • レスポンス本文を最後まで受信し
  • JSON としてパースする
という処理が行われます。
処理の流れを図で表すと
fetch(url) 開始
   ↓
[ 通信中… ]
   ↓
レスポンスヘッダ受信   ← await fetch() はここで完了
   ↓
本文受信中(ストリーム)
   ↓
JSON パース完了        ← await res.json() はここまで待つ

典型的な正しい書き方
const res = await fetch(url);

if (!res.ok) {
  // ステータスコードはこの時点で確認できる
  throw new Error(`HTTP ${res.status}`);
}

// 本文を最後まで読むのはここ
const data = await res.json();
この順番が重要です。
よくある誤解①
await fetch() だけで、 JSON まで全部受け取っていると思ってしまうこと。
await fetch(url);
// ここで JSON はまだ読まれていない
これは誤解です。 本文は res.json() を await したときに初めて読み終わります。
よくある誤解②
await が Node.js 全体を止めていると思ってしまうこと。

await は:
  • この 関数の中の処理順 を止めるだけ
  • Node.js のイベントループは止まらない
  • 他のリクエストや処理は普通に進む

Android Java と感覚を対応づける

Android Java(例:OkHttp)での同期呼び出し:

Response res = client.newCall(request).execute();
これは:
  • スレッドをブロックする
  • ヘッダも本文もまとめて待つ
Node.js の await fetch() は:
  • スレッドをブロックしない
  • まずヘッダまで待つ
  • 本文は別の await で待つ
という点が決定的に違います。
この理解が重要な理由
この挙動を正しく理解していないと、
  • ステータスチェックの位置を間違える
  • 巨大レスポンスで無駄にメモリを使う
  • 非同期処理の流れが分からなくなる
といった問題が起きます。

await fetch() は「通信完了」ではなく、
「レスポンスが返り始めたところまで」を待っている
—— これが正確な理解です。

同じ処理を .then().catch() で書くとどうなる?(対応関係が見える版)

async/await は「Promise を then/catch で書けるものを、同期っぽく書ける構文」です。
つまり、さっきの await fetchExternalUsers(...) は、Promise で言うと .then(...) に対応します。
そして try/catch.catch(...) に対応します。

Controller を then/catch で書いた版(同じ機能)
import type { Request, Response, NextFunction } from "express";
import { fetchExternalUsers } from "../services/externalUserService";
import type { PublicUser } from "../types/externalUserTypes";

const EXTERNAL_BASE_URL = process.env.EXTERNAL_BASE_URL ?? "https://other.example.com";

export function getAllUsersThen(req: Request, res: Response, next: NextFunction) {
  fetchExternalUsers(EXTERNAL_BASE_URL)
    .then((external) => {
      const list: PublicUser[] = external.users.map(u => ({
        id: u.userid,
        name: u.username,
        email: u.email,
      }));
      res.json({ users: list });
    })
    .catch((err) => {
      next(err);
    });
}
📘 対応関係(ここが知りたいポイント) async/await 版
try {
  const external = await fetchExternalUsers(...);
  res.json(...);
} catch (err) {
  next(err);
}
then/catch 版
fetchExternalUsers(...)
  .then((external) => {
    res.json(...);
  })
  .catch((err) => {
    next(err);
  });
  • await XX.then(...)(成功時の処理)
  • try/catch.catch(...)(失敗時の処理)
  • throw new Error(...) = Promise で言うと reject(失敗として伝える)

実務で「Router/Controller/Service」を分ける理由(要約ではなく理由を明確に)


このサンプルの入出力(何が返るのか)

他サーバーから受け取る JSON(例):

{
  "users": [
    { "userid": 1, "username": "john", "email": "john@company.com", "tel": "080-1721-0000", "nickname": "yobochan" },
    { "userid": 2, "username": "alice", "email": "alice@company.com", "tel": "090-0000-1111", "nickname": "ali" }
  ]
}

自サーバーが返す JSON(必要項目だけにした結果):

{
  "users": [
    { "id": 1, "name": "john",  "email": "john@company.com" },
    { "id": 2, "name": "alice", "email": "alice@company.com" }
  ]
}
💡 次に学ぶと一気に実務っぽくなる改善点 外部JSONは本当は unknown で受けて、型ガード(検証)してから扱うのが堅いです。
ここは学習段階として「最低限の形チェック」だけ入れています。

6. ファイルI/O と DBアクセス(SQLite3)— Node.js/Express/TypeScript 実務入門

この章では、Node.js バックエンドで避けて通れない ファイルI/ODBアクセスを、 初心者でも「何を・なぜ・どう書くか」が分かるように、コードを多めに示しながら説明します。


6-1. ファイルI/O(File Input/Output)とは

ファイルI/O とは「ファイルを読む/書く」ことです。Node.js ではディスクアクセスは遅い(待ち時間がある)ため、 基本的に 非同期 で扱います。

📘 用語 fs:Node.js のファイル操作ライブラリ(標準)。
Promise版APIfs/promises のこと。await で読み書きできる。
ストリーム:巨大ファイルを「少しずつ」読み書きする仕組み。メモリ節約になる。

6-2. まずは基本:テキストファイルの読み書き(fs/promises)

6-2-1. 文字列として読む(小さめのファイル向け)

import { readFile } from "node:fs/promises";

async function loadText(path: string): Promise<string> {
  // encoding を指定すると string で返る
  const text = await readFile(path, { encoding: "utf-8" });
  return text;
}
💡 何をしている? readFile はファイル全体を読み込みます。小さめ(数MB程度まで)のテキストに向きます。
大きいファイルを readFile で読むと、メモリを大量に使うので注意。

6-2-2. 文字列として書く(上書き)

import { writeFile } from "node:fs/promises";

async function saveText(path: string, text: string): Promise<void> {
  await writeFile(path, text, { encoding: "utf-8" });
}

6-2-3. 追記する(ログなど)

import { appendFile } from "node:fs/promises";

async function appendLog(path: string, line: string): Promise<void> {
  await appendFile(path, line + "\n", { encoding: "utf-8" });
}

6-2-4. ファイルが存在するか(例外で判定しない書き方)

import { access } from "node:fs/promises";
import { constants } from "node:fs";

async function exists(path: string): Promise<boolean> {
  try {
    await access(path, constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

6-3. パス操作:Windows/Linux で壊れない書き方

文字列連結で "dir/" + file を作ると、区切り文字の違い(Windowsは \)などで壊れやすいです。
必ず node:path を使います。

import path from "node:path";

// __dirname の代わり(ESM/TS設定によって変わるが、例として)
const dataDir = path.resolve(process.cwd(), "data");
const filePath = path.join(dataDir, "users.json");

6-4. JSONファイルを読む(型とセットで)

外部入力(ファイル内容)は「信用しない」が基本です。本来は unknown で受けて検証します。
ここでは学習用に「最低限の形チェック」を入れた例を示します。

import { readFile } from "node:fs/promises";

type UserJson = {
  userid: number;
  username: string;
  email: string;
};

function isUserJson(x: any): x is UserJson {
  return x
    && typeof x.userid === "number"
    && typeof x.username === "string"
    && typeof x.email === "string";
}

export async function loadUsersJson(path: string): Promise<UserJson[]> {
  const text = await readFile(path, { encoding: "utf-8" });
  const raw: unknown = JSON.parse(text);

  if (!Array.isArray(raw)) throw new Error("JSON is not an array");

  const list: UserJson[] = [];
  for (const item of raw) {
    if (!isUserJson(item)) throw new Error("Invalid user item in JSON");
    list.push(item);
  }
  return list;
}

7. SQLite3 DBアクセス(Node.js/Express/TypeScript)

SQLite は「DBサーバーが不要」で、アプリと同じPC内の 1つのファイル(例:app.db)に保存します。
配布が簡単で、小〜中規模のアプリやローカル用途に強いです。

📘 用語 SQL:DB操作言語(SELECT/INSERT/UPDATE/DELETE)。
パラメータバインド:SQL文字列に値を安全に埋め込む仕組み(SQL注入対策)。
トランザクション:複数の更新を「全部成功 or 全部失敗」にまとめる仕組み(整合性)。
コネクション:DBへの接続。SQLiteは「ファイルを開く」ことに近い。
コネクションプール:接続を使い回して高速化する仕組み(SQLiteでは通常不要/概念が違う)。

7-1. 使用ライブラリ方針(初心者向けのおすすめ)

SQLite を Node.js から使う方法はいくつかあります。学習のしやすさ重視で以下を採用します。

これにより await db.get() / await db.all() のように書けます。

💡 インストール例
npm i express
npm i -D typescript ts-node-dev @types/express

npm i sqlite3 sqlite

7-2. フォルダ構成(DB層を分ける)

src/
  app.ts
  routes/
    userRoutes.ts
  controllers/
    userController.ts
  services/
    externalUserService.ts
  db/
    sqlite.ts
    userRepository.ts
    migrate.ts
  types/
    userTypes.ts

7-3. DB接続(sqlite.ts)

SQLiteはDBサーバーへ接続するのではなく、DBファイルを開きます。
代表的には「アプリ起動時に開いて、使い回す」設計にします。

// src/db/sqlite.ts
import path from "node:path";
import sqlite3 from "sqlite3";
import { open, type Database } from "sqlite";

let db: Database<sqlite3.Database, sqlite3.Statement> | null = null;

export async function getDb(): Promise<Database<sqlite3.Database, sqlite3.Statement>> {
  if (db) return db;

  const dbPath = path.resolve(process.cwd(), "app.db"); // 配布しやすいようプロジェクト直下に置く例
  db = await open({
    filename: dbPath,
    driver: sqlite3.Database,
  });

  // 実務では推奨される設定:外部キー制約を有効化
  await db.exec("PRAGMA foreign_keys = ON;");

  return db;
}
💡 なぜ「使い回す」の? SQLiteで毎回 open/close を繰り返すと遅くなりやすいです。
「起動時に開く → 使い回す」が分かりやすく、トラブルも少ないです。

7-4. テーブル作成(migrate.ts)

SQLiteは最初にテーブルを作らないと使えません。学習では「起動時にCREATEしておく」でも良いですが、
実務では「マイグレーション」として分けることが多いです。

// src/db/migrate.ts
import { getDb } from "./sqlite";

export async function migrate(): Promise<void> {
  const db = await getDb();

  await db.exec(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      email TEXT NOT NULL UNIQUE
    );
  `);
}

app.ts 側で起動時に呼びます:

// src/app.ts(抜粋)
import express from "express";
import { migrate } from "./db/migrate";
import { userRouter } from "./routes/userRoutes";

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

await migrate(); // 起動時にDB準備(学習用に分かりやすい)

app.use("/user", userRouter);

app.listen(3000);

7-5. Repository(SQLをまとめる層)

ControllerにSQLが散らばると読みにくくなります。
そこで「DBアクセスはRepositoryに集約」するのが一般的です。

// src/types/userTypes.ts
export type UserRow = {
  id: number;
  name: string;
  email: string;
};

export type NewUser = {
  name: string;
  email: string;
};
// src/db/userRepository.ts
import { getDb } from "./sqlite";
import type { NewUser, UserRow } from "../types/userTypes";

export async function insertUser(u: NewUser): Promise<number> {
  const db = await getDb();

  // パラメータバインド:SQL注入対策 + 型の安全性
  const result = await db.run(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    u.name,
    u.email
  );

  // sqliteのrunは lastID を返す
  return result.lastID as number;
}

export async function getAllUsers(): Promise<UserRow[]> {
  const db = await getDb();
  return await db.all<UserRow[]>("SELECT id, name, email FROM users ORDER BY id");
}

export async function getUserById(id: number): Promise<UserRow | null> {
  const db = await getDb();
  const row = await db.get<UserRow>("SELECT id, name, email FROM users WHERE id = ?", id);
  return row ?? null;
}
⚠ SQL注入(SQL Injection)を避ける 値を SQL に文字列連結してはいけません:
"... WHERE id = " + id は危険。必ず ? のパラメータを使います。

7-6. トランザクション(複数更新をまとめる)

たとえば「2件のINSERTが両方成功したらOK。1件でも失敗したら全部取り消す」という場面があります。
これをトランザクションで書きます。

// src/db/userRepository.ts(追記例)
import type { NewUser } from "../types/userTypes";
import { getDb } from "./sqlite";

export async function insertTwoUsers(a: NewUser, b: NewUser): Promise<void> {
  const db = await getDb();
  await db.exec("BEGIN");

  try {
    await db.run("INSERT INTO users (name, email) VALUES (?, ?)", a.name, a.email);
    await db.run("INSERT INTO users (name, email) VALUES (?, ?)", b.name, b.email);

    await db.exec("COMMIT");
  } catch (e) {
    await db.exec("ROLLBACK");
    throw e;
  }
}
💡 await と try/catch の関係 await した Promise が失敗(reject)すると、そこで例外になります。
だからトランザクションの commit/rollback は try/catch と相性が良いです。

7-7. Express ルート(Router → Controller → Repository → Response)

// src/controllers/userController.ts
import type { Request, Response, NextFunction } from "express";
import * as repo from "../db/userRepository";

export async function getAllUsers(req: Request, res: Response, next: NextFunction) {
  try {
    const users = await repo.getAllUsers();
    res.json({ users });
  } catch (e) {
    next(e);
  }
}

export async function createUser(req: Request, res: Response, next: NextFunction) {
  try {
    // 外部入力なので本来は unknown → 検証が理想。学習用に最小チェックだけ。
    const name = String(req.body?.name ?? "");
    const email = String(req.body?.email ?? "");

    if (!name || !email) {
      return res.status(400).json({ error: "name and email are required" });
    }

    const id = await repo.insertUser({ name, email });
    res.status(201).json({ id });
  } catch (e) {
    next(e);
  }
}
// src/routes/userRoutes.ts
import { Router } from "express";
import { getAllUsers, createUser } from "../controllers/userController";

export const userRouter = Router();

userRouter.get("/get/all", getAllUsers);
userRouter.post("/create", createUser);

7-8. SQLiteの「コネクションプール」はどう考える?

Oracle/MySQL/PostgreSQL/SQL Server のようなDBサーバーでは、 接続(コネクション)を作るのが重いので コネクションプールが重要です。
一方で SQLite は「ファイルを開く」イメージに近く、一般的な意味でのプールは通常使いません。

⚠ SQLiteの同時書き込み SQLite は1つのDBファイルを共有するため、書き込みが多い高負荷サーバーでは詰まりやすいです。
「ローカル用途」「小〜中規模」「配布が簡単」が強みです。

8. どのDBに接続するか・プール・主要DBの違い

8-1. DBを選ぶ観点(初心者向け)

8-2. サーバーDBのコネクションプール(雰囲気だけでも掴む)

サーバーDBは「毎回 connect すると遅い」ので、起動時にプールを作り、クエリごとに借りて返します。

PostgreSQL(pg)

import { Pool } from "pg";

const pool = new Pool({
  host: "localhost",
  port: 5432,
  user: "app",
  password: "secret",
  database: "appdb",
  max: 10, // プール内コネクション数
});

export async function query(sql: string, params: any[]) {
  return await pool.query(sql, params);
}

MySQL(mysql2)

import mysql from "mysql2/promise";

const pool = mysql.createPool({
  host: "localhost",
  user: "app",
  password: "secret",
  database: "appdb",
  connectionLimit: 10,
});

export async function query(sql: string, params: any[]) {
  const [rows] = await pool.execute(sql, params);
  return rows;
}

Oracle(oracledb)

import oracledb from "oracledb";

// 起動時にプールを作る
const pool = await oracledb.createPool({
  user: "app",
  password: "secret",
  connectString: "localhost/XEPDB1",
  poolMin: 1,
  poolMax: 10,
});

export async function query(sql: string, binds: any[] = []) {
  const conn = await pool.getConnection();
  try {
    const result = await conn.execute(sql, binds, { outFormat: oracledb.OUT_FORMAT_OBJECT });
    return result.rows;
  } finally {
    await conn.close();
  }
}

SQL Server(mssql)

import sql from "mssql";

const pool = await sql.connect({
  server: "localhost",
  user: "app",
  password: "secret",
  database: "appdb",
  options: { encrypt: false },
  pool: { max: 10, min: 1 },
});

export async function query(text: string, params: Record<string, any> = {}) {
  const req = pool.request();
  for (const [k, v] of Object.entries(params)) req.input(k, v);
  const result = await req.query(text);
  return result.recordset;
}
💡 プールが必要な理由 DB接続は「ソケット確立」「認証」「初期化」などが重く、毎回作ると遅い。
プールは接続を使い回すことで、性能と安定性を上げます。

8-3. Oracle / MySQL / PostgreSQL / SQL Server の違い(初心者向けまとめ)

DB特徴注意/相性
PostgreSQL 機能が強い・拡張性が高い・オープンソースで人気 SQLが厳密寄り。JSON型や拡張が便利
MySQL 採用例が多い・運用ノウハウが豊富 ストレージエンジン等の理解が必要になることがある
SQL Server Microsoft環境で強い・企業で多い T-SQL(方言)やWindows運用の文化がある
Oracle 大規模・堅牢・企業基幹で強い ライセンス/運用が重め。方言も多い
⚠ SQLの「方言」 基本は同じSQLでも、DBごとに方言(書き方の差)があります。
例:LIMIT/OFFSET の扱い、日付関数、UPSERT構文など。
「DBをまたいで動くSQL」を書きたい場合は、後述の ORM/Query Builder が効きます。

9. SQL以外でアクセスできる?(Spring Boot の @Entity 的なもの)

はい、できます。Node/TypeScript の世界では、 ORM(Object-Relational Mapping)や Query Builder を使うと、 SQLを直接書かず(または最小限で)DB操作できます。

📘 用語 ORM:テーブルを「クラス/モデル」に対応付けて操作する仕組み(SpringのJPAに近い)。
Query Builder:SQL文字列を直接書かず、メソッドチェーンで組み立てる仕組み。

9-1. TypeORM(デコレータで @Entity っぽく書ける)

TypeORM は @Entity のようなデコレータで定義します(Springの感覚に近い)。
※ 環境・設定が増えるため、学習初期は「存在を知る」程度でもOKです。

// 例:TypeORMの雰囲気(概念)
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  name!: string;

  @Column({ unique: true })
  email!: string;
}

9-2. Prisma(型安全が強い・実務人気)

Prisma は schema ファイルから型を生成し、TypeScriptから安全に操作できます。
「SQLを全く書かない」より「必要ならSQLも使える」方向の設計です。

// Prismaの雰囲気(概念)
const users = await prisma.user.findMany({
  select: { id: true, name: true, email: true }
});

9-3. Sequelize(歴史が長いORM) / Knex(Query Builder)

Sequelize は ORM、Knex は Query Builder の代表格です。
「SQLを減らしたい」「複数DBに対応したい」場合の候補になります。

💡 ORMを使う/使わないの基準
  • 学習や小規模:SQL直書き(この章のSQLite方式)は理解が早い
  • 中〜大規模:ORM/Query Builder は保守性・型安全・移植性で効く
  • 複雑なSQL:結局SQLが必要な場面もある(ORMでも生SQLを許可することが多い)

10. 章のまとめ

📘 SQLite の WAL モードとロック(同時アクセスの注意)

SQLite は「軽量で手軽」なデータベースですが、
同時アクセス(並行処理)については RDBMS(MySQL / PostgreSQL など)とは考え方が違います。


1. SQLite は「ファイルを直接ロックするDB」

SQLite はサーバープロセスを持たず、 1つの DB ファイル(.sqlite / .db)を直接読み書きします。

そのため、同時に複数の書き込みが発生すると、 ロック競合が起きやすくなります。


2. デフォルト(ROLLBACK JOURNAL)モードの問題点

SQLite の初期設定は ROLLBACK JOURNAL モードです。

このモードでは:

// 悪い例(同時アクセス時)
Request A: INSERT → 書き込みロック取得
Request B: SELECT → 「database is locked」

Web API では、
「書き込み中に read が来る」のは日常茶飯事なので、 この挙動は致命的です。


3. WAL(Write-Ahead Logging)モードとは?

そこで使うのが WAL モード です。

WAL は名前の通り:

「まずログに書いてから、あとでまとめて本体に反映する」

仕組みを簡単に言うと:

DB本体: app.db
WALログ: app.db-wal
共有メモリ: app.db-shm

4. WAL モードの最大のメリット

実務で SQLite を使うなら、
WAL モードは「ほぼ必須」です。


5. WAL モードの設定方法(必ず最初に実行)

// 起動時に1回だけ実行する
db.exec("PRAGMA journal_mode = WAL;");
db.exec("PRAGMA synchronous = NORMAL;");
⚠ 注意 WAL は DBを開いた直後に設定してください。
途中で切り替えると効果が分かりにくくなります。

6. WAL でも「書き込みは1つだけ」

誤解しやすい点ですが:

WAL = 書き込みが無限に並列になる ではありません。

つまり:


7. Node.js でよくある「database is locked」の原因

// 悪い例
db.run("BEGIN");
heavyCalculation(); // ← ここでロック長時間保持
db.run("COMMIT");

8. 正しいトランザクションの使い方

// 良い例
db.run("BEGIN");
db.run("INSERT ...");
db.run("INSERT ...");
db.run("COMMIT");

9. SQLite が向いているケース / 向いていないケース

用途向き・不向き
個人ツール / 管理画面
小規模Web API◎(WAL必須)
同時書き込みが多い
高トラフィックSNS

10. 他DB(MySQL / PostgreSQL)との決定的な違い


まとめ(重要)
SQLite は「軽くて便利」ですが、 同時アクセスには制約があるDBです。

Webアプリで使うなら:

「SQLite はダメ」ではなく、
設計を理解して使えば非常に優秀な選択肢です。

📘 SQLite のキューイング(書き込み直列化)とは何か

SQLite は 同時に複数の書き込みができない データベースです。 WAL モードを使っても、 「同時 WRITE は常に 1 つだけ」という制約は変わりません。

そのため Web API(Node.js / Express)では、 書き込み処理をキュー(順番待ち)にして直列化する設計が重要になります。


1. なぜキューイングが必要なのか

Node.js は 並行リクエストを自然に処理します。

// 同時に来る
POST /user/create
POST /user/create
POST /user/create

これらが同時に INSERT を実行すると:

SQLITE_BUSY: database is locked

というエラーが ランダムに発生します。

👉 これを防ぐ唯一の確実な方法が 「書き込みを1本の列に並ばせる」ことです。


2. キューイングの基本発想

「SQLite に書き込む処理は、
必ず 前の書き込みが終わってから 実行する」

これは DB の制限であり、 アプリ側で吸収すべき責務です。


3. 最もシンプルな方法:Promise チェーン

// 書き込み専用キュー
let writeQueue = Promise.resolve();

function enqueueWrite(task) {
  writeQueue = writeQueue.then(() => task())
                         .catch(err => {
                           console.error("write error:", err);
                         });
  return writeQueue;
}

この enqueueWrite を通した書き込みは、 必ず順番に実行されます。


4. 実際の SQLite 書き込み例

// sqlite3 / better-sqlite3 共通イメージ
enqueueWrite(() => {
  return new Promise((resolve, reject) => {
    db.run(
      "INSERT INTO users(name, email) VALUES (?, ?)",
      ["john", "john@company.com"],
      (err) => {
        if (err) reject(err);
        else resolve();
      }
    );
  });
});

ポイント:


5. Express の Controller での使い方

export async function createUser(req, res) {
  const { name, email } = req.body;

  await enqueueWrite(() => {
    return new Promise((resolve, reject) => {
      db.run(
        "INSERT INTO users(name, email) VALUES (?, ?)",
        [name, email],
        err => err ? reject(err) : resolve()
      );
    });
  });

  res.json({ ok: true });
}

複数リクエストが同時に来ても:

という順序が保証されます。


6. トランザクションもキューの中で行う

enqueueWrite(() => {
  return new Promise((resolve, reject) => {
    db.serialize(() => {
      db.run("BEGIN");
      db.run("INSERT ...");
      db.run("INSERT ...");
      db.run("COMMIT", err =>
        err ? reject(err) : resolve()
      );
    });
  });
});

BEGIN〜COMMIT 全体を1タスクとして扱うのが重要です。


7. よくある NG パターン

// 悪い例
db.run("BEGIN");
await heavyLogic(); // ← ロック保持
db.run("COMMIT");

8. better-sqlite3 を使う場合(補足)

better-sqlite3 は同期 API のため、 JS の実行順 = 書き込み順になります。

そのため:

の場合は、暗黙的にキュー化されます。 ただし:


9. SQLite キューイングの限界


10. 他DBとの考え方の違い

DB書き込み並列キュー必要?
SQLite1つ必須
MySQL複数不要
PostgreSQL複数不要
Oracle複数不要

まとめ(重要)
SQLite を Web API で使うなら:

SQLite は「小規模・低〜中頻度 WRITE」では 非常に優秀な DBです。 キューイングは、その性能を引き出すための 必須テクニックです。

📘 テンポラリーファイルは OS の違いを超えて使えるか?

結論から言うと、
Node.js を使えば、Windows / macOS / Linux の違いを意識せずに テンポラリーファイルを安全に利用できます。

ただし、それは 「正しい API を使った場合に限る」という前提があります。


1. OS ごとの「一時ディレクトリ」の違い

各 OS には「一時ファイルを置く標準ディレクトリ」があります。

OS代表的な一時ディレクトリ
WindowsC:\Users\xxx\AppData\Local\Temp
macOS/var/folders/...
Linux/tmp

これらを ハードコードしてはいけません。 環境・権限・実行ユーザーによって変わるためです。


2. Node.js の正解:os.tmpdir()

Node.js には、OS に依存しない 公式 API が用意されています。

import os from "os";

const tmpDir = os.tmpdir();
console.log(tmpDir);

この os.tmpdir() は:

👉 OS差分を超える唯一の正解ルートです。


3. テンポラリーファイルの安全な作り方

一時ファイルは「名前衝突」と「セキュリティ」に注意が必要です。

❌ やってはいけない例
// 危険:ファイル名固定
const path = os.tmpdir() + "/temp.txt";

✅ 正しい例(fs.mkdtemp)
import fs from "fs/promises";
import os from "os";
import path from "path";

const baseTmp = os.tmpdir();

// 一意な一時ディレクトリを作る
const tmpDir = await fs.mkdtemp(
  path.join(baseTmp, "myapp-")
);

const tmpFile = path.join(tmpDir, "upload.bin");

await fs.writeFile(tmpFile, buffer);

この方法のメリット:


4. 大きなファイルでは「ストリーム」が基本

大容量ファイル(画像・動画)では、 メモリに全部載せてはいけません

import fs from "fs";
import path from "path";

const writeStream = fs.createWriteStream(tmpFile);
req.pipe(writeStream);

writeStream.on("finish", () => {
  console.log("一時保存完了");
});

この方法は:


5. 後片付け(重要)

テンポラリーファイルは 必ず削除する設計にします。

// ディレクトリごと削除
await fs.rm(tmpDir, { recursive: true, force: true });

削除漏れがあると:


6. Express + multer との関係

multer は内部的に:

つまり、multer を正しく使えば OS差分はすでに吸収されています。

💡 実務ヒント 本番では memoryStorage より diskStorage + テンポラリーディレクトリが安全です。

7. コンテナ(Docker)環境での注意


8. 他言語(Java / Spring Boot)との対応

言語一時ディレクトリ取得
Node.jsos.tmpdir()
JavaSystem.getProperty("java.io.tmpdir")
Pythontempfile.gettempdir()

考え方は すべて同じです。


まとめ(重要)

これを守れば、 Windows / macOS / Linux を意識せずに 安全なテンポラリーファイル運用ができます。

💡 fs.mkdtemp() は「毎回ちがうフォルダ」を作る

次のコードは、同じフォルダを作っているように見えて実際には毎回ちがう一時ディレクトリを作ります。

const tmpDir = await fs.mkdtemp(
  path.join(baseTmp, "myapp-")
);

"myapp-"名前の前半(プレフィックス)にすぎません。
fs.mkdtemp() は内部で 衝突しないランダム文字列を自動付与します。

// 実際に作られる例
/tmp/myapp-a8F3kL
/tmp/myapp-Qm2L8R
/tmp/myapp-0xM7Wc

そのため、たとえ中に置くファイル名が同じでも:

/tmp/myapp-a8F3kL/upload.bin
/tmp/myapp-Qm2L8R/upload.bin

フルパスは毎回ユニークになり、
同時実行でも衝突しません。

一時ファイルでは 「ファイル名で一意性を出す」のではなく、 「親ディレクトリを一意にする」のが安全な定石です。

11. HTMLからのファイルアップロード / ダウンロード(大きい画像ファイル想定)

ここでは「ブラウザのHTMLフォームから画像をアップロードし、サーバー側で保存し、 後でダウンロード(または表示)できる」までを、Router から順に説明します。
画像は大きいファイルを想定するため、Node.js では メモリに丸ごと載せない(ストリーミング)設計が重要です。

📘 この節で出てくる用語 multipart/form-data:ファイルを送るときのHTTP形式(HTMLフォームがこれを使う)。
ストリーミング:データを少しずつ読み書きする(巨大ファイルでもメモリ爆発しない)。
multer:Expressでファイルアップロードを扱う定番ミドルウェア(内部でストリーム処理)。
Content-Disposition:ダウンロード時にファイル名を指定するためのヘッダ。

11-1. 方針(大きいファイルで重要なこと)


11-2. 事前準備(ライブラリ)

# 画像アップロード処理に multer を使う
npm i multer
npm i -D @types/multer

11-3. まず Router から(/file/upload, /file/download/:id)

ルーティングは次の2つを用意します:

// src/routes/fileRoutes.ts
import { Router } from "express";
import { uploadSingleImage, downloadById } from "../controllers/fileController";

export const fileRouter = Router();

// HTMLフォームからのアップロード
fileRouter.post("/upload", uploadSingleImage);

// ID を指定してダウンロード(または表示)
fileRouter.get("/download/:id", downloadById);

app.ts 側でベースパスを付けるなら:

// src/app.ts(抜粋)
import express from "express";
import { fileRouter } from "./routes/fileRoutes";

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

// /file/... を fileRouter に委譲
app.use("/file", fileRouter);

app.listen(3000);
💡 URL が /file/upload になる理由 app.use("/file", fileRouter)(ベースパス) + fileRouter.post("/upload")(相対パス) = /file/upload

11-4. アップロード設定(multer:ディスク保存、サイズ制限、ファイル名)

大きい画像を扱うため、メモリ保存(memoryStorage)ではなく diskStorage を使います
また、攻撃や事故を防ぐため、ファイルサイズ上限も必ず設定します。

// src/middlewares/upload.ts
import multer from "multer";
import path from "node:path";
import crypto from "node:crypto";
import { mkdirSync } from "node:fs";

const uploadDir = path.resolve(process.cwd(), "uploads");

// 起動時に uploads フォルダが無ければ作る
mkdirSync(uploadDir, { recursive: true });

function safeExt(originalName: string): string {
  // 拡張子は信用しすぎない(ここでは最低限)
  const ext = path.extname(originalName).toLowerCase();
  if (ext === ".png" || ext === ".jpg" || ext === ".jpeg" || ext === ".webp") return ext;
  return ".bin";
}

const storage = multer.diskStorage({
  destination: (_req, _file, cb) => cb(null, uploadDir),
  filename: (_req, file, cb) => {
    // ファイル名衝突を避けるためランダムID + 拡張子
    const id = crypto.randomUUID();
    cb(null, `${id}${safeExt(file.originalname)}`);
  },
});

export const uploadImage = multer({
  storage,
  // 大きい画像想定:例として 50MB(必要に応じて調整)
  limits: { fileSize: 50 * 1024 * 1024 },
  fileFilter: (_req, file, cb) => {
    // MIME type チェック(完全ではないが最低限)
    const ok = ["image/png", "image/jpeg", "image/webp"].includes(file.mimetype);
    if (!ok) return cb(new Error("Only png/jpeg/webp are allowed"));
    cb(null, true);
  },
});
⚠ なぜ拡張子や mimetype を信用しすぎてはいけない? 悪意あるアップロードでは「画像に見せかけた別ファイル」が送られることがあります。
本格的にやるなら「ファイルの先頭バイト(マジックナンバー)」で判定するライブラリを追加します。
ただし学習段階では、まず サイズ制限mimetype制限を入れるのが第一歩です。

11-5. Controller(アップロード:受け取って保存し、IDを返す)

multer は Express のミドルウェアとして動き、
ファイルは保存済みの状態で req.file に情報が入ります。

// src/controllers/fileController.ts
import type { Request, Response, NextFunction } from "express";
import path from "node:path";
import { uploadImage } from "../middlewares/upload";
import { stat } from "node:fs/promises";
import { createReadStream } from "node:fs";

type UploadResponse = {
  id: string;
  originalName: string;
  storedName: string;
  size: number;
  mime: string;
  downloadUrl: string;
};

// ここがポイント:Router から直接 controller を呼ぶと multer が挟めないので、
// controller 側で「upload.single(...) を1回呼ぶ」形にします。
// (もちろん Router 側で upload.single(...) を挟んでもOKです)
export function uploadSingleImage(req: Request, res: Response, next: NextFunction) {
  const middleware = uploadImage.single("image"); // HTML側 input name="image" と合わせる

  middleware(req, res, async (err: any) => {
    try {
      if (err) return next(err);
      if (!req.file) return res.status(400).json({ error: "file is required (image)" });

      // 保存ファイル名(例: UUID.jpg)
      const storedName = req.file.filename;
      const id = path.parse(storedName).name; // UUID 部分だけ取り出す

      const fileStat = await stat(req.file.path);

      const body: UploadResponse = {
        id,
        originalName: req.file.originalname,
        storedName,
        size: fileStat.size,
        mime: req.file.mimetype,
        downloadUrl: `/file/download/${id}`,
      };

      res.status(201).json(body);
    } catch (e) {
      next(e);
    }
  });
}

function resolveStoredPath(id: string): string {
  // uploads/ の中から「idで始まるファイル」を探す…などもあるが、
  // 学習用に「id + 任意拡張子」を許すため、ここでは簡単化します。
  // 実務では DB か メタ情報ファイルで id→filename を管理するのが確実です。
  const uploadDir = path.resolve(process.cwd(), "uploads");

  // ここではよくある拡張子を順番に試す(簡易版)
  // ※ 本格的には「アップロード時に id と storedName を SQLite に保存」するのが正解です。
  const candidates = [".png", ".jpg", ".jpeg", ".webp", ".bin"].map(ext => path.join(uploadDir, `${id}${ext}`));
  return candidates[0]; // ダウンロード側で実際に存在確認しつつ探す
}

export async function downloadById(req: Request, res: Response, next: NextFunction) {
  try {
    const id = String(req.params.id ?? "");

    if (!id) return res.status(400).json({ error: "id is required" });

    // 簡易:拡張子候補を探す
    const uploadDir = path.resolve(process.cwd(), "uploads");
    const exts = [".png", ".jpg", ".jpeg", ".webp", ".bin"];

    let foundPath: string | null = null;
    let foundExt: string | null = null;

    for (const ext of exts) {
      const p = path.join(uploadDir, `${id}${ext}`);
      try {
        await stat(p);
        foundPath = p;
        foundExt = ext;
        break;
      } catch {
        // not found
      }
    }

    if (!foundPath) return res.status(404).json({ error: "file not found" });

    // Content-Type(画像表示 or ダウンロードのヒント)
    const contentType =
      foundExt === ".png" ? "image/png" :
      foundExt === ".webp" ? "image/webp" :
      (foundExt === ".jpg" || foundExt === ".jpeg") ? "image/jpeg" :
      "application/octet-stream";

    res.setHeader("Content-Type", contentType);

    // ダウンロードさせたい場合は Content-Disposition を付ける
    // 画像をブラウザ表示させたいなら "inline"、保存させたいなら "attachment"
    res.setHeader("Content-Disposition", `attachment; filename="${id}${foundExt}"`);

    // 大きいファイル対応:ストリームで返す(readFileで全部読まない)
    const stream = createReadStream(foundPath);
    stream.on("error", next);
    stream.pipe(res);

  } catch (e) {
    next(e);
  }
}
⚠ 重要:大きいファイルは readFile しない readFile はファイル全体をメモリに読みます。大きい画像だとメモリが危険です。
ここでは createReadStream(...).pipe(res)ストリーム送信しています。

11-6. Router で multer を挟む書き方(別解:こちらの方が一般的)

さっきは controller 内で uploadImage.single(...) を呼びましたが、
実務では Router 側でミドルウェアとして挟む方が読みやすいことが多いです。

// src/routes/fileRoutes.ts(別案)
import { Router } from "express";
import { uploadImage } from "../middlewares/upload";
import { uploadSingleImage, downloadById } from "../controllers/fileController";

export const fileRouter = Router();

// Routerで multer を挟む(この形が一般的)
fileRouter.post("/upload", uploadImage.single("image"), uploadSingleImage);

fileRouter.get("/download/:id", downloadById);

この場合、Controller の uploadSingleImage は「保存済みファイル情報を返すだけ」になります:

// src/controllers/fileController.ts(uploadSingleImage を簡略化できる)
export async function uploadSingleImage(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.file) return res.status(400).json({ error: "file is required (image)" });

    const storedName = req.file.filename;
    const id = path.parse(storedName).name;

    const fileStat = await stat(req.file.path);

    res.status(201).json({
      id,
      originalName: req.file.originalname,
      storedName,
      size: fileStat.size,
      mime: req.file.mimetype,
      downloadUrl: `/file/download/${id}`,
    });
  } catch (e) {
    next(e);
  }
}
💡 どっちが良い? 初心者には「Routerでミドルウェアを挟む」方が、責務が分かれて見通しが良いことが多いです。
Router:流れを組み立てる / Controller:処理する という分担になります。

11-7. HTML(ブラウザ側):アップロードフォームの例

HTMLからファイルを送るときは enctype="multipart/form-data" が必須です。
また <input type="file" name="image">name はサーバー側の single("image") と一致させます。

<form action="/file/upload" method="post" enctype="multipart/form-data">
  <label>画像を選択:</label>
  <input type="file" name="image" accept="image/png,image/jpeg,image/webp" />

  <button type="submit">アップロード</button>
</form>

11-7-1. fetch で送る版(進捗表示やUI制御に向く)

HTMLフォーム送信は簡単ですが、送信完了後に画面が遷移します。
UIを作るなら JavaScript の fetchFormData を使うと便利です。


    <input id="file" type="file" accept="image/*" />
<button id="send">送信</button>

<script>
document.getElementById("send").addEventListener("click", async () => {
  const fileInput = document.getElementById("file");
  const file = fileInput.files?.[0];
  if (!file) {
    alert("ファイルを選択してください");
    return;
  }

  const fd = new FormData();
  fd.append("image", file); // multer.single("image") と合わせる

  const res = await fetch("/file/upload", { method: "POST", body: fd });
  const json = await res.json();

  if (!res.ok) {
    alert("失敗: " + (json?.error ?? "unknown"));
    return;
  }

  alert("成功。ダウンロードURL: " + json.downloadUrl);
});
</script>
💡 大きいファイルのUIの話 fetch だけだと「アップロード進捗」を取りにくいです。
進捗が必要なら XMLHttpRequest を使うか、より新しいAPI/ライブラリで対応します(実務話)。

11-8. 大きいファイルで必須の安全策(実務の現実)

⚠ 本格運用の注意(参考) 高負荷・大量アップロードでは、サーバーのディスク容量・I/O性能がボトルネックになります。
実務では S3 などのオブジェクトストレージに直接アップロードさせる設計もよくあります。

11-9. ここまでの動作確認(手順)

  1. サーバー起動(例:npm run dev
  2. ブラウザでアップロードフォームを開く
  3. 画像を選択して送信 → JSONで iddownloadUrl が返る
  4. downloadUrl(例:/file/download/<id>)にアクセスするとファイルが落ちる

1. ルーターはどこに増やす?

ルーターは URLのまとまり単位で増やします。 迷ったら「URLの第1階層」を基準にします。

src/
  routes/
    user.router.ts
    auth.router.ts
    file.router.ts
  app.ts

app.ts では、ルーターを「マウント」するだけにします。

// app.ts
app.use("/user", userRouter);
app.use("/auth", authRouter);
app.use("/file", fileRouter);
💡 判断基準 「このURL群は別チーム・別機能になりそうか?」
YESならルーターを分けます。

2. Controller / Service / Repository の分け方

最初から完璧に分ける必要はありません。
「HTTPの話」と「業務処理」が混ざり始めたら分離が合図です。

src/
  controllers/
    user.controller.ts
  services/
    user.service.ts
  repositories/
    user.repository.ts
💡 初心者向け指針 「res.json() を書いているなら Controller」
「SQL が出てきたら Repository」

3. 例外処理と HTTP エラーの考え方

原則: Service / Repository では例外を投げるだけ。 HTTP に変換するのは Controller より後です。

HTTPエラー用の型

export class HttpError extends Error {
  constructor(
    public status: number,
    message: string
  ) {
    super(message);
  }
}

Service 側

if (!user) {
  throw new HttpError(404, "user not found");
}

async route の例外をまとめて拾う

// error middleware
app.use((err, req, res, next) => {
  if (err instanceof HttpError) {
    res.status(err.status).json({ error: err.message });
    return;
  }

  console.error(err);
  res.status(500).json({ error: "internal server error" });
});
⚠ 重要 予期しない例外は 必ず 500 にし、
詳細はレスポンスに出さずログに出します。

4. 入力バリデーション(型は実行時に消える)

TypeScript の型は コンパイル時だけ存在します。
HTTP body は「信用してはいけません」。

最低限のやり方

function isUser(x: unknown): x is { name: string } {
  return typeof x === "object"
    && x !== null
    && typeof (x as any).name === "string";
}

zod を使う場合(おすすめ)

const UserSchema = z.object({
  name: z.string(),
  email: z.string().email()
});

const user = UserSchema.parse(req.body);
💡 腑に落ちポイント 「型があるのに壊れる」のは、
実行時に型が無いからです。

5. 環境変数と .env

開発と本番で値が変わるものは、 コードに書かないのが原則です。

// .env(開発用)
PORT=3000
DB_PATH=./data/app.db
// 読み込み
import "dotenv/config";
const port = process.env.PORT;

6. 最低限のセキュリティ

app.use(helmet());
app.use(cors());
app.use(rateLimit({ windowMs: 60000, max: 100 }));

7. VS Code デバッグ(launch.json)

sourceMap: true を設定しているので、 TypeScript のままデバッグできます。

{
  "type": "node",
  "request": "launch",
  "name": "Debug API",
  "program": "${workspaceFolder}/src/app.ts",
  "runtimeArgs": ["-r", "ts-node/register"]
}

8. 最小のテスト(GET /health)

it("health check", async () => {
  await request(app)
    .get("/health")
    .expect(200);
});

これがあるだけで、 「壊れていない」保証が手に入ります。

1. ファイルは分けるのが普通か?

結論から言うと:

✅ 実務では「分ける」のが普通 特に class責務を持つ interface1ファイル1定義が基本です。

よくある構成例

src/
  models/
    user.ts           // interface User
  entities/
    user.entity.ts    // class UserEntity
  enums/
    user-role.ts     // enum UserRole
  services/
    user.service.ts  // class UserService

理由はシンプルです:

💡 例外 小さなプロジェクトや PoC では、
関連する interface / enum を同一ファイルにまとめることもあります。

2. interface / type / enum はどこに置く?

判断基準は「どの層の概念か」です。

種類置き場所の例意味
DTO interface models/ API入出力用の形
Entity class entities/ DBと対応する実体
enum enums/ 状態・種別
内部用 type 同一ファイル内 実装詳細
// models/user.ts
export interface User {
  id: number;
  name: string;
}
// enums/user-role.ts
export enum UserRole {
  Admin = "admin",
  User = "user"
}

3. public / protected / private はある?

はい、あります
TypeScript の class は Java や C# とほぼ同じアクセス修飾子を持ちます。

class UserService {
  public findAll() {
    return this.load();
  }

  protected load() {
    return [];
  }

  private validate() {
    // 内部処理
  }
}
修飾子意味
publicどこからでもアクセス可(省略時のデフォルト)
protected自分 + 継承先のみ
privateクラス内部のみ
💡 初心者向け指針 まずは private を積極的に使うと設計が壊れにくいです。

4. Java / C# との違い(重要)

TypeScript のアクセス修飾子は:

⚠ 実行時には存在しない 型チェック用であり、 JavaScript 実行時に強制されるわけではありません。

つまり:

それでも使う理由は:

📌 まとめ

TypeScript では、
「何を公開するか」「どこに置くか」設計で決めるのが重要です。

言語が自由だからこそ、
ルールを自分で作ると初心者でも破綻しません。

Node.js に DI(Dependency Injection)はあるのか?

結論から言うと:

✅ Node.js でも DI は「できる」 ただし Spring Boot のような標準DIコンテナは存在しません
代わりに「設計としてのDI」を自分で選択します。

1. DI(Dependency Injection)とは

DI とは:

「クラスや関数が必要とする依存物を、
自分で new せず、外から渡す設計」

目的は次の3つです。

2. Node.js で最も一般的な DI(手動DI)

Node.js では、コンストラクタ引数で依存を渡すのが王道です。

// repository
export class UserRepository {
  findAll() {
    return [];
  }
}

// service
export class UserService {
  constructor(private repo: UserRepository) {}

  findAll() {
    return this.repo.findAll();
  }
}

// controller
const repo = new UserRepository();
const service = new UserService(repo);
💡 これも DI 「DI = フレームワーク」ではありません。
依存を外から渡していれば、それはDIです。

3. interface を使った DI

TypeScript では、interface + DI が強力です。

// repository interface
export interface UserRepository {
  findAll(): User[];
}

// 実装
export class SqliteUserRepository implements UserRepository {
  findAll() { return []; }
}

// service
export class UserService {
  constructor(private repo: UserRepository) {}
}

これにより:

4. Node.js に DI コンテナはある?

あります。ただし 必須ではありません

ライブラリ特徴
tsyringe軽量・初心者向け
InversifyJSSpring風・高機能
NestJSフレームワーク一体型DI
⚠ 初心者への注意 いきなり DI コンテナを入れると、
「なぜ動くのか分からないコード」になります。

5. Spring Boot との違い

Spring BootNode.js
DI 標準 設計として実装
@Autowired ある ない
設定 自動 明示的

Node.js では:

「魔法は少ないが、
依存関係がコードから読める」

6. 初心者向けおすすめ

✅ 最初はこれで十分
  • コンストラクタ引数で依存を渡す
  • interface で境界を切る
  • new はアプリ起動時にまとめる
  • DI コンテナは後から

このやり方は:

📌 まとめ

Node.js に DI は「あります」。
ただしそれは フレームワークではなく設計です。

まずは 手動DI + interface を理解すれば、
Spring 的な DI も自然に理解できるようになります。

publicの実務での正しいルール

TypeScriptでクラスを外部に見えるようにするには、export を使います。
しかし、実務では public はメソッドやプロパティに使い、クラス自体には付けないのが一般的です。

TypeScript では、 class 宣言は 常に public 扱いです。

✅ 実務ルール
  • export で公開制御
  • public はメソッド・プロパティに使う
  • class には付けない
// 良い例
export class UserService {
  public findAll() {}
  private validate() {}
}

import { UserService } from "./user.service";

const service = new UserService();
service.findAll();
💡 理由 TypeScript のアクセス修飾子は「型チェック用」であり、
Java や C# のように実行時に強制されるわけではありません。
そのため、クラス自体を public にする意味は薄いです。

例外処理(Exception Handling)

Node.js + Express + TypeScript では、 「どこで例外を投げ、どこで HTTP エラーにするか」 を決めておかないと、コードがすぐに壊れます。

このセクションでは、実務で最も一般的な考え方として:

順番に説明します。


1. なぜ例外処理を決める必要があるのか

初心者が書きがちなコードでは、次のような問題が起きます。

原因はシンプルで:

「誰が、何の責任で例外を処理するのか」 が決まっていない

そこで、レイヤーごとに 責務を明確に分けます

2. レイヤーごとの例外責務

レイヤー 役割 例外の扱い
Repository DB / 外部I/O 技術的な例外を そのまま throw
(DBエラー、接続失敗など)
Service 業務ロジック 意味のある失敗を 例外として表現
(NotFound / Validation など)
Controller HTTP 層 原則 例外を処理しない
(投げて middleware に任せる)
✅ 重要ルール Controller では try/catch を書かないのが基本です。

3. HTTP エラーを表す型 HttpError

業務的な失敗(404 / 400 / 403 など)は、 普通の Error とは区別したくなります。

そのために、HTTP ステータスを持つエラー型を作ります。

// errors/http-error.ts
export class HttpError extends Error {
  public readonly status: number;

  constructor(status: number, message: string) {
    super(message);
    this.status = status;
  }
}

これにより、

例外そのものに意味として持たせることができます。

4. Service で意味のある例外を投げる

// services/user.service.ts
import { HttpError } from "../errors/http-error";

export class UserService {
  async findById(id: number) {
    const user = null; // 仮

    if (!user) {
      throw new HttpError(404, "User not found");
    }

    return user;
  }
}

ここでは:

という点が重要です。

5. Controller は例外を捕まえない

// controllers/user.controller.ts
import { Request, Response } from "express";
import { UserService } from "../services/user.service";

const service = new UserService();

export async function getUser(req: Request, res: Response) {
  const id = Number(req.params.id);

  const user = await service.findById(id);
  res.json(user);
}

ここには try/catch がありません。

throw された例外は、 Express が自動で error middleware に渡します

6. error middleware で例外を一括処理

Express では、 引数が4つある middleware がエラー専用です。

// app.ts
import express, { Request, Response, NextFunction } from "express";
import { HttpError } from "./errors/http-error";

const app = express();

app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
  if (err instanceof HttpError) {
    res.status(err.status).json({ error: err.message });
    return;
  }

  // 想定外の例外
  console.error("Unexpected error:", err);
  res.status(500).json({ error: "Internal Server Error" });
});
⚠ 重要 予期しない例外は必ず 500 にし、
ログは必ず出すのが実務の基本です。

7. 例外処理の全体像

  1. Repository が技術的エラーを throw
  2. Service が意味のある失敗を HttpError として throw
  3. Controller は例外を触らない
  4. error middleware が HTTP レスポンスに変換
📌 覚え方 「例外は下から上へ、HTTP は一番上で」

用語集(このマニュアル内で使う Node/TS 専門用語)

用語定義
Node.js JavaScript をサーバー側で実行するための実行環境(ランタイム)。
Express Node.js上で動くWebフレームワーク。HTTPルーティングやミドルウェアを提供する。
npm Node.jsのパッケージ管理ツール。依存のインストール、スクリプト実行などを行う。
npx インストール済みパッケージのコマンドを、パスを通さずに実行する仕組み。
package.json プロジェクト設定ファイル。依存、スクリプト、名前、バージョン等を保持。
tsc TypeScriptコンパイラ。TS→JSへの変換(トランスパイル)を行う。
tsconfig.json TypeScriptコンパイル設定。入力(src)や出力(dist)、厳密さなどを指定。
dist ビルド出力先フォルダーの慣例名。TypeScript から生成された JavaScript を置く。
devDependencies 開発時のみ必要な依存。例:TypeScript、型定義、テストツールなど。
nodemon ファイル変更を監視してプロセスを自動再起動するツール。
ts-node TypeScript を(ビルドせずに)実行する補助ツール。開発向き。
CommonJS Node.jsで長く使われたモジュール方式(require/module.exports)。TSの出力形式として指定できる。
ESM (ES Modules) 標準のモジュール方式(import/export)。Node.js側の設定次第で挙動が変わる。
環境変数 コードを書き換えずに設定(PORTなど)を渡す仕組み。実行環境ごとの切替に使う。
Source Map 変換後JSから、元のTSの行番号へ紐付ける情報。デバッグやスタックトレースで効く。

仕様書セクション

1. 概要

このサンプルは、実際に存在しそうな小さな Web APIを題材に、

をすべて含めた構成で、 例外処理がどこで起き、どこでまとめて処理されるのかを理解することを目的とします。

2. エンドポイント仕様

MethodPath概要
GET /health 疎通確認用。常に OK を返す
GET /users DB からユーザー一覧を取得
POST /users/:id/avatar ユーザーの画像をアップロード
GET /users/:id/external 外部 API から追加情報を取得

3. 処理フロー図(例外の流れ)


[HTTP Request]
      |
      v
[Controller]
  (HTTP入出力のみ)
      |
      v
[Service]
  (業務判断)
  ├─ 不正な入力 → HttpError(400)
  ├─ データなし → HttpError(404)
      |
      v
[Repository]
  ├─ DBエラー
  ├─ File I/O エラー
  ├─ 外部 HTTP エラー
      |
      v
[throw Error / HttpError]
      |
      v
[Error Middleware]
  ├─ HttpError → 指定 status
  └─ 想定外 → 500 + ログ
  
💡 覚え方 例外は 下で起きて上でまとめる
HTTP ステータスは 最後に決める

4. フォルダ構成


src/
 ├─ app.ts
 ├─ routes/
 │    └─ user.router.ts
 ├─ controllers/
 │    └─ user.controller.ts
 ├─ services/
 │    └─ user.service.ts
 ├─ repositories/
 │    ├─ user.repository.ts
 │    └─ external.repository.ts
 ├─ errors/
 │    └─ http-error.ts
 └─ middlewares/
      └─ error.middleware.ts
  

サンプルコードセクション

app.ts


// Express を読み込む
import express from "express";
// ルーターを読み込む
import { userRouter } from "./routes/user.router";
// エラーミドルウェアを読み込む
import { errorMiddleware } from "./middlewares/error.middleware";

// Express アプリを作成
const app = express();

// JSON body を読む設定
app.use(express.json());

// ヘルスチェック
app.get("/health", (_req, res) => {
  res.json({ ok: true, time: new Date().toISOString() });
});

// /users 配下を userRouter に委譲
app.use("/users", userRouter);

// 例外を最後にまとめて処理
app.use(errorMiddleware);

// ポート番号を決定
const port = Number(process.env.PORT ?? 3000);

// サーバー起動
app.listen(port, () => {
  console.log(`listening on http://localhost:${port}`);
});

errors/http-error.ts


// HTTP 用のエラー型
export class HttpError extends Error {
  // HTTP ステータス
  public readonly status: number;

  // コンストラクタ
  constructor(status: number, message: string) {
    super(message);      // Error に message を渡す
    this.status = status; // status を保存
  }
}

repositories/user.repository.ts


// DB アクセスを担当する Repository
export class UserRepository {
  async findAll() {
    // 本来は SQLite だが、ここでは簡略化
    return [
      { id: 1, name: "john", email: "john@test.com" }
    ];
  }
}

repositories/external.repository.ts


// 外部 HTTP API を呼び出す Repository
export class ExternalRepository {
  async fetchExtraInfo(id: number) {
    // 疑似的に外部 API 失敗を再現
    if (id === 0) {
      throw new Error("external api failed");
    }
    return { score: 42 };
  }
}

services/user.service.ts


// Service は業務ロジックを担当
import { UserRepository } from "../repositories/user.repository";
import { ExternalRepository } from "../repositories/external.repository";
import { HttpError } from "../errors/http-error";

// Service クラス
export class UserService {
  // Repository を保持
  constructor(
    private userRepo = new UserRepository(),
    private externalRepo = new ExternalRepository()
  ) {}

  // ユーザー一覧取得
  async getUsers() {
    return this.userRepo.findAll();
  }

  // 外部情報取得
  async getExternalInfo(id: number) {
    if (id <= 0) {
      throw new HttpError(400, "invalid user id");
    }
    try {
      return await this.externalRepo.fetchExtraInfo(id);
    } catch {
      throw new HttpError(502, "external service error");
    }
  }
}

controllers/user.controller.ts


// Controller は HTTP 入出力のみ担当
import { Request, Response } from "express";
import { UserService } from "../services/user.service";

// Service を生成
const service = new UserService();

// ユーザー一覧
export async function getUsers(_req: Request, res: Response) {
  const users = await service.getUsers();
  res.json(users);
}

// 外部情報取得
export async function getExternal(req: Request, res: Response) {
  const id = Number(req.params.id);
  const info = await service.getExternalInfo(id);
  res.json(info);
}

routes/user.router.ts


// Router 定義
import { Router } from "express";
import { getUsers, getExternal } from "../controllers/user.controller";

// Router を作成
export const userRouter = Router();

// 一覧取得
userRouter.get("/", getUsers);

// 外部情報取得
userRouter.get("/:id/external", getExternal);

middlewares/error.middleware.ts


// エラーミドルウェア
import { Request, Response, NextFunction } from "express";
import { HttpError } from "../errors/http-error";

// error middleware
export function errorMiddleware(
  err: unknown,
  _req: Request,
  res: Response,
  _next: NextFunction
) {
  if (err instanceof HttpError) {
    res.status(err.status).json({ error: err.message });
    return;
  }
  console.error("unexpected error:", err);
  res.status(500).json({ error: "internal server error" });
}
✅ このサンプルで分かること
  • Webアプリは I/O の集合体である
  • 例外は Repository / Service で起きる
  • Controller は静かでいい
  • HTTP ステータスは最後に決める

仕様説明セクション:注文・請求ルールエンジン

1. このサンプルで学ぶこと

このサンプルは、Webアプリの「通信」よりも、 TypeScript の文法を業務ロジックの中でどう使うかに焦点を当てています。

「文法の説明」ではなく、 実際の業務ルールの中で文法がどう使われるか を理解するのが目的です。

2. 業務の概要

ユーザーから渡された「注文データ」を元に、 以下を計算します。

3. 入力データの考え方

入力は JavaScript オブジェクト(JSON 相当)です。 実務では HTTP body に相当します。

重要なポイント:

4. 主な業務ルール

税計算

割引

送料

💡 なぜ enum を使うのか 税区分や会員ランクは「文字列」ではなく、 業務上の決まった選択肢です。
enum を使うことで、 タイプミスや想定外の値を防げます。

5. 入力 / 出力データ仕様(コメント付き)

入力(注文データ)

業務ロジックに渡される入力データです。
実務では HTTP リクエストの body(JSON)に相当します。


// ※ 説明用のためコメント付き(実際の JSON ではコメント不可)
{
  "customerRank": "GOLD",        // 顧客ランク(enum: BRONZE / SILVER / GOLD)
  "region": "TOKYO",             // 配送地域(enum: TOKYO / OTHER)

  "items": [                     // 注文行の配列
    {
      "sku": "A001",              // 商品コード
      "unitPrice": 980,           // 単価(number)
      "qty": 2,                   // 数量(number)
      "tax": "STANDARD"           // 税区分(enum)
    },
    {
      "sku": "B777",
      "unitPrice": 1500,
      "qty": 1,
      "tax": "REDUCED"
    }
  ],

  "coupon": "WELCOME10"          // クーポンコード(任意・optional)
}
💡 ポイント
  • items は配列なので for...of で処理する
  • customerRanktax は enum で安全に扱う
  • couponあってもなくてもよい(optional)

出力(計算結果)

業務ロジックが返す計算結果です。
API のレスポンスや画面表示用データにそのまま使えます。


// 計算結果(出力)
{
  "subtotal": 3460,        // 税抜小計(全行の subtotal 合計)
  "taxTotal": 292,         // 税額合計
  "discount": 346,         // 割引合計(会員 + クーポン)
  "shipping": 500,         // 送料
  "grandTotal": 3906       // 最終請求金額
}
💡 計算の流れ
  1. 各行の subtotal を計算
  2. 税区分ごとに税額を算出
  3. 小計を reduce で集計
  4. 割引・送料を適用
  5. 最終合計を算出
⚠ 実務上の注意 TypeScript の型は 実行時には存在しません
実際の API では、この入力データを unknown として受け取り、 バリデーションしてから処理します。

コードセクション:文法サンプル集(業務ロジック)

1. enum 定義(業務区分)


// 顧客ランクを表す enum
enum CustomerRank {
  BRONZE = "BRONZE",
  SILVER = "SILVER",
  GOLD = "GOLD"
}

// 税区分を表す enum
enum TaxType {
  STANDARD = "STANDARD",
  REDUCED = "REDUCED",
  EXEMPT = "EXEMPT"
}

// 配送地域
enum Region {
  TOKYO = "TOKYO",
  OTHER = "OTHER"
}

2. データ構造(interface)


// 注文行
interface OrderItem {
  sku: string;          // 商品コード
  unitPrice: number;   // 単価
  qty: number;         // 数量
  tax: TaxType;        // 税区分
}

// 注文全体
interface Order {
  customerRank: CustomerRank; // 顧客ランク
  items: OrderItem[];         // 注文行の配列
  coupon?: string;            // クーポン(任意)
  region: Region;             // 配送地域
}

3. 税計算(switch / enum)


// 税額を計算する関数
function calcTax(amount: number, taxType: TaxType): number {
  switch (taxType) {
    case TaxType.STANDARD:
      return Math.floor(amount * 0.10);
    case TaxType.REDUCED:
      return Math.floor(amount * 0.08);
    case TaxType.EXEMPT:
      return 0;
  }
}

4. 注文明細計算(for...of)


// 注文明細を計算する
function calcLines(items: OrderItem[]) {
  const results = []; // 結果配列

  // 配列を1件ずつ処理
  for (const item of items) {
    const subtotal = item.unitPrice * item.qty; // 行小計
    const tax = calcTax(subtotal, item.tax);    // 税額
    const total = subtotal + tax;                // 行合計

    results.push({
      sku: item.sku,
      subtotal,
      tax,
      total
    });
  }

  return results;
}

5. 割引計算(if / enum)


// 割引額を計算する
function calcDiscount(subtotal: number, rank: CustomerRank, coupon?: string): number {
  let discount = 0;

  // 会員ランク割引
  if (rank === CustomerRank.GOLD) {
    discount += Math.floor(subtotal * 0.05);
  }

  // クーポン割引
  if (coupon === "WELCOME10") {
    discount += Math.floor(subtotal * 0.10);
  }

  return discount;
}

6. 送料計算


// 送料を計算する
function calcShipping(subtotal: number, region: Region): number {
  if (subtotal >= 10000) {
    return 0;
  }

  return region === Region.TOKYO ? 500 : 800;
}

7. 全体処理(map / reduce)


// 注文全体を処理する
function processOrder(order: Order) {
  const lines = calcLines(order.items);

  // 小計を集計
  const subtotal = lines.reduce((sum, l) => sum + l.subtotal, 0);

  // 税合計を集計
  const taxTotal = lines.reduce((sum, l) => sum + l.tax, 0);

  // 割引計算
  const discount = calcDiscount(subtotal, order.customerRank, order.coupon);

  // 送料計算
  const shipping = calcShipping(subtotal, order.region);

  // 最終合計
  const grandTotal = subtotal + taxTotal + shipping - discount;

  return {
    subtotal,
    taxTotal,
    discount,
    shipping,
    grandTotal
  };
}
✅ このサンプルで理解できること
  • 文法は「業務ルールを表現する道具」
  • enum は業務区分を安全に表す
  • for...of は明示的で読みやすい
  • map / reduce は集計に向いている

TypeScript の List / Map / Set / 配列 と 基本型のメソッド

1. TypeScript に「List」はある?

TypeScript(JavaScript)には、 Java や C# のような List クラスはありません。

その代わり、配列(Array) が List の役割を担います。


// List の代わりになる配列
const list: number[] = [1, 2, 3];
💡 実務感覚 Java/C# の List = TypeScript の Array

2. 配列(Array)

2-1. 配列はサイズ変更可能か?

はい、可能です。
JavaScript の配列は 可変長(動的配列)です。


const arr: number[] = [1, 2];

// 要素を追加
arr.push(3);        // [1, 2, 3]

// 要素を削除
arr.pop();          // [1, 2]

// 途中に追加
arr.splice(1, 0, 99); // [1, 99, 2]
💡 const でも変更できる理由 const は「参照の再代入禁止」であり、
中身の変更は禁止していません

2-2. よく使う配列メソッド

メソッド用途
push末尾に追加
pop末尾を削除
map変換
filter絞り込み
reduce集計
find1件検索

const nums = [1, 2, 3];

// map: 各要素を変換
const doubled = nums.map(n => n * 2); // [2, 4, 6]

// filter: 条件に合うものだけ
const even = nums.filter(n => n % 2 === 0); // [2]

// reduce: 集計
const sum = nums.reduce((a, b) => a + b, 0); // 6

3. Map(キーと値の集合)

Map は「キー → 値」の対応を持つコレクションです。


const userMap = new Map();

userMap.set(1, "john");
userMap.set(2, "alice");

userMap.get(1); // "john"

オブジェクトとの違い


4. Set(重複しない集合)

Set重複を許さない コレクションです。


const ids = new Set();

ids.add(1);
ids.add(1);
ids.add(2);

ids.size; // 2

「既にあるか?」の判定が高速です。


5. string / number にメソッドはある?

あります。
プリミティブ型でも、内部的にはオブジェクトとして扱われます。

5-1. string の主なメソッド

メソッド説明
length文字数
includes部分一致
startsWith前方一致
split分割
replace置換
match正規表現マッチ

const s = "hello world";

s.length;               // 11
s.includes("world");    // true
s.split(" ");            // ["hello", "world"]

5-2. number の主なメソッド


const n = 3.14159;

n.toFixed(2);   // "3.14"
n.toString();   // "3"

6. 正規表現の使い方

正規表現は RegExp オブジェクトとして使います。


const email = "john@test.com";
const regex = /^(.+)@(.+)$/;

const match = email.match(regex);

6-1. グループの値を取る


if (match) {
  match[0]; // "john@test.com"(全体)
  match[1]; // "john"(1番目のグループ)
  match[2]; // "test.com"(2番目のグループ)
}
💡 実務での使いどころ
  • メールアドレス検証
  • ログ解析
  • 入力フォーマットチェック

7. まとめ

TypeScript の文法は、 データを安全に・読みやすく扱うための道具です。

📘 Array.reduce を「業務ロジックで使える」レベルまで理解する

reduce は配列を 1つの値にたたみ込む(集約する)ためのメソッドです。
「合計を出す」だけではなく、集計・辞書化・グループ化・変換など、 業務ロジックの中心でよく使われます。


1) reduce の基本形(引数の意味)

// 配列.reduce( (acc, cur, index, array) => 次のacc, 初期acc )
const result = array.reduce((acc, cur, index, arr) => {
  // acc : 途中経過(累積値)
  // cur : 今処理している要素
  // index : 何番目か
  // arr : 元配列
  return acc; // 次のループに渡す累積値
}, initialValue);
  • acc(accumulator):累積値(途中経過の箱)
  • cur(current):今の要素
  • initialValue:累積値の初期値(型が決まるので重要)
💡 初期値は「ほぼ必須」 初期値を省略すると、最初の要素が acc になり、
空配列でエラーになったり、型がブレたりして初心者が混乱します。
実務では 初期値を必ず書くのが安全です。

2) まずは王道:合計(number に畳み込む)

const nums = [10, 20, 30];

// acc は number、初期値 0 から開始
const sum = nums.reduce((acc, cur) => {
  return acc + cur;
}, 0);

console.log(sum); // 60

ここでのポイントは「acc が常に number」だと確定することです。
初期値 0 があることで、TypeScript も確実に推論できます。


3) 業務で最頻出:配列の合計(注文の小計・税合計など)

type Line = { subtotal: number; tax: number };

const lines: Line[] = [
  { subtotal: 1960, tax: 196 },
  { subtotal: 1500, tax: 96 }
];

// 小計合計
const subtotal = lines.reduce((acc, line) => acc + line.subtotal, 0);

// 税合計
const taxTotal = lines.reduce((acc, line) => acc + line.tax, 0);

console.log(subtotal); // 3460
console.log(taxTotal); // 292

配列(行)→ 合計値」の形は、請求・在庫・勤怠などあらゆる業務で出ます。


4) reduce が本領発揮する場面①:辞書化(id → データ)

例えば「ユーザー配列を、id で即引ける辞書(Map風のオブジェクト)にしたい」ケースです。

type User = { id: number; name: string };

const users: User[] = [
  { id: 1, name: "john" },
  { id: 2, name: "alice" }
];

// Record<number, User> はキーが number の辞書型
const byId = users.reduce((acc, u) => {
  acc[u.id] = u;         // 辞書に登録
  return acc;            // 次のaccに渡す
}, {} as Record<number, User>);

console.log(byId[2].name); // "alice"
💡 Map を使う版(より安全) Object 辞書はキーが文字列化されるため、
実務では new Map() にするほうが安全なことも多いです。
const map = users.reduce((acc, u) => {
  acc.set(u.id, u);
  return acc;
}, new Map<number, User>());

console.log(map.get(2)?.name); // "alice"

5) reduce が本領発揮する場面②:グループ化(カテゴリ別集計)

「税区分ごとの合計」「部署ごとの人数」「状態ごとの件数」などはグループ化の代表例です。

type Item = { tax: "STANDARD" | "REDUCED"; amount: number };

const items: Item[] = [
  { tax: "STANDARD", amount: 1000 },
  { tax: "STANDARD", amount: 500 },
  { tax: "REDUCED", amount: 1500 }
];

// taxごとに合計を集計する
const sumByTax = items.reduce((acc, it) => {
  acc[it.tax] = (acc[it.tax] ?? 0) + it.amount;
  return acc;
}, {} as Record<"STANDARD" | "REDUCED", number>);

console.log(sumByTax); // { STANDARD: 1500, REDUCED: 1500 }

acc[it.tax] ?? 0 は「まだ無ければ 0」を意味します(nullish coalescing)。


6) reduce が本領発揮する場面③:複数値をまとめて返す(集計オブジェクト)

reduce の「畳み込み先(acc)」は number だけでなく、オブジェクトにもできます。
これにより、1回のループで複数の集計値を作れます。

type Line = { subtotal: number; tax: number };

const lines: Line[] = [
  { subtotal: 1960, tax: 196 },
  { subtotal: 1500, tax: 96 }
];

const summary = lines.reduce((acc, line) => {
  acc.subtotal += line.subtotal;
  acc.taxTotal += line.tax;
  acc.count += 1;
  return acc;
}, { subtotal: 0, taxTotal: 0, count: 0 });

console.log(summary); // { subtotal: 3460, taxTotal: 292, count: 2 }
⚠ reduce を使いすぎない reduce は強力ですが、読みづらくなりやすいです。
特に「途中で if が増える」「ネストが深い」場合は、
for...of にした方が初心者にも実務にも読みやすいことが多いです。

7) map / filter / reduce の使い分け(覚え方)

  • map:要素を変換して同じ長さの配列を作る(1件→1件)
  • filter:条件に合う要素だけ残す(間引き)
  • reduce:配列を1つの結果に畳み込む(合計・辞書・集計・グループ化)
✅ 実務での判断基準(迷ったら)
  • 「合計・集計・辞書化」なら reduce
  • 「途中で break したい」「条件分岐が多い」なら for...of
  • 「読みやすさが最優先」なら for...of(初心者教材では特に)

追補:immutable(不変)・配列操作の実務アンチパターン・Map/Setの使い分け

8. immutable(不変)に書くとは?

「immutable(不変)」とは、元のデータを直接書き換えず新しいデータを作って返す書き方です。

なぜ重要か:

8-1. const と immutable は別物

const は「変数に別の値を再代入できない」だけで、 オブジェクトや配列の中身は変更できます

// const でも中身は変えられる
const a = [1, 2];
a.push(3); // OK(中身が変わる)

8-2. 配列を immutable に扱う例(おすすめ)

// 追加(push しない)
const a = [1, 2];
const b = [...a, 3];           // [1, 2, 3](aは変わらない)

// 先頭追加
const c = [0, ...a];           // [0, 1, 2]

// 置き換え(index 1 を 99 に)
const d = a.map((x, i) => i === 1 ? 99 : x);  // [1, 99]

// 削除(index 1 を削除)
const e = a.filter((_, i) => i !== 1);        // [1]

8-3. オブジェクトを immutable に扱う例

const user = { id: 1, name: "john", email: "a@b.com" };

// 変更(元は変えない)
const user2 = { ...user, email: "new@b.com" };
💡 実務ルール(迷ったら)
  • 「関数の外から渡された配列・オブジェクト」は原則 mutate しない
  • 「自分で作ったローカル変数」だけ mutate を許す(性能が必要な場面)

9. 配列操作の実務アンチパターン(やりがち注意)

9-1. forEachreturn しても止まらない

初心者が混乱しやすい点です。forEachreturn は 「コールバックから戻る」だけで、ループを止めません。

// ❌ 止まらない
[1, 2, 3].forEach(n => {
  if (n === 2) return; // 2のときだけスキップ(中断ではない)
  console.log(n);
});

// ✅ 中断したいなら for...of
for (const n of [1, 2, 3]) {
  if (n === 2) break;  // ここで中断できる
  console.log(n);
}

9-2. map で副作用だけやる

map は「変換して新しい配列を返す」ための関数です。 返り値を捨てて副作用だけに使うのは避けます。

// ❌ map の結果を使っていない
items.map(x => console.log(x));

// ✅ 副作用なら for...of / forEach
for (const x of items) {
  console.log(x);
}

9-3. ループ中に splice で削除して index が崩壊

途中削除は index がずれてバグりやすいです。基本は filter

// ❌ バグりがち(削除でindexがずれる)
for (let i = 0; i < arr.length; i++) {
  if (arr[i] < 0) arr.splice(i, 1);
}

// ✅ filter で新配列
const cleaned = arr.filter(x => x >= 0);

9-4. sort は元配列を破壊する

sortin-place(元配列を書き換え)なので注意です。

const a = [3, 1, 2];

// ❌ a が書き換わる
a.sort();

// ✅ コピーしてから sort(immutable)
const b = [...a].sort();

9-5. == を使う(型変換の罠)

// ❌ 型変換が起きる
0 == "0" // true

// ✅ ふつうは厳密比較
0 === "0" // false
⚠ 実務の鉄則 配列操作は「破壊的か?非破壊的か?」を必ず意識します。
破壊的:push / pop / splice / sort / reverse
非破壊的:map / filter / reduce / slice / concat / spread

10. Map / Set を使うべきケース・使わないケース

10-1. まずは配列で十分なケース

10-2. Map を使うべきケース(キー検索が中心)

「id を渡したら一発で見つけたい」ケースは Map が強いです。 配列の find は毎回先頭から探すため、件数が増えると遅くなります。

// 配列で探す(毎回O(n))
const users = [{ id: 1, name: "john" }, { id: 2, name: "alice" }];
const u = users.find(x => x.id === 2);

// Map にする(検索O(1)に近い)
const userMap = new Map();
for (const x of users) {
  userMap.set(x.id, x);
}
const u2 = userMap.get(2);

10-3. Set を使うべきケース(重複排除・存在チェック)

// 重複排除
const ids = [1, 1, 2, 3, 3];
const unique = [...new Set(ids)]; // [1, 2, 3]

// 存在チェック
const allowed = new Set(["jpg", "png", "webp"]);
allowed.has("png"); // true

10-4. Map/Set を使う時の注意(参照同一性)

オブジェクトを Set/Map のキーにする場合は、 「同じ内容」ではなく「同じ参照」かどうかで判定されます。

const a = { x: 1 };
const b = { x: 1 };

const s = new Set<object>();
s.add(a);

s.has(a); // true
s.has(b); // false(内容が同じでも別オブジェクト)
✅ 実務の使い分けまとめ

11. string / number メソッドの実務的まとめ(追加)

「一覧」と言うと膨大になるため、実務で頻出のものを目的別に整理します。 ここに載っていないメソッドもありますが、まずはこれで十分です。

11-1. string(検索・切り出し・整形)

目的メソッド
長さlength"abc".length
部分一致includess.includes("x")
前方/後方一致startsWith, endsWiths.startsWith("http")
切り出しslice, substrings.slice(0, 3)
分割splits.split(",")
置換replace, replaceAlls.replace("a","b")
空白処理trim, trimStart, trimEnds.trim()
大小文字toLowerCase, toUpperCases.toLowerCase()
const s = "  Hello,World  ";

const trimmed = s.trim();               // "Hello,World"
const parts = trimmed.split(",");        // ["Hello", "World"]
const hello = parts[0].toLowerCase();    // "hello"
const world = parts[1].toUpperCase();    // "WORLD"

11-2. number(表示・丸め・変換)

メソッド用途
toFixed小数点桁を固定(文字列になる)(3.1).toFixed(2)
toString文字列化(10).toString()
const n = 3.14159;

const s1 = n.toFixed(2); // "3.14"(文字列)
const s2 = n.toString(); // "3"

変換はメソッド以外にも頻出です:

Number("123"); // 123
String(123);   // "123"
parseInt("10", 10); // 10
parseFloat("3.14"); // 3.14

12. 正規表現:グループ取得までを体系的に

12-1. 基本:match とグループ

const text = "john@company.com";

// () がキャプチャグループ
const re = /^(.+)@(.+)$/;

const m = text.match(re);

if (m) {
  m[0]; // "john@company.com"(全体)
  m[1]; // "john"(1番目のグループ)
  m[2]; // "company.com"(2番目のグループ)
}

12-2. 名前付きグループ(取る側が読みやすい)

const text = "2026-02-15";

// (?<name>...) が名前付きグループ
const re = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/;

const m = text.match(re);

if (m && m.groups) {
  m.groups.year;  // "2026"
  m.groups.month; // "02"
  m.groups.day;   // "15"
}

12-3. 複数ヒット:matchAll

const log = "id=1 name=john; id=2 name=alice;";
const re = /id=(\d+)\s+name=([a-z]+)/g;

// matchAll はイテレータなので for...of と相性が良い
for (const m of log.matchAll(re)) {
  const id = Number(m[1]);
  const name = m[2];
  console.log(id, name);
}
💡 正規表現の実務ポイント
  • 入力検証は「正規表現だけ」に頼りすぎない(読みづらくなる)
  • ログ解析・パターン抽出には強い
  • 名前付きグループを使うと保守が楽
❓ 質問:ユーザー設定を session に入れるのは普通?

例えば「ユーザーの画面設定(テーマ・言語など)を、ログイン時に DB から取ってきて session に入れておく」のは、Node.js(Express)では普通でしょうか?
それとも、画面から毎回「設定取得API」を呼ぶのが普通でしょうか?

結論(Node.js / Express でも同じ結論)

「ユーザー設定ファイルを丸ごと session に入れる」のは、Node.js(Express)でも 普通ではない ことが多いです。
多くの現場では 状況に応じて分ける のが普通です。
一般的には、画面は必要に応じて設定取得APIを呼ぶ(ただしキャッシュ併用)が多いです。

まず前提整理(Node.js の session はこういう性質)

Express の session(例:express-session)は:

  • ユーザーごとに state(状態)を保持するので、サーバー側のメモリ or ストアを消費します
  • 複数台構成(クラスタ/スケールアウト)では、共有ストア(Redis 等)がほぼ必須になります
  • 中身が大きいほど、シリアライズ/デシリアライズコスト・ネットワーク転送(Redis等)・レイテンシが増えます

なので
👉 「設定ファイルだから session に入れとけ」は 雑設計 と見られがちです。

パターン別に整理します

① ユーザー設定を session に入れるパターン

使われることはあるが、限定的

向いているケース

  • ログイン後ほぼ 全画面/全APIで使う
  • サイズが 小さい(数KB以下)
  • 毎回 DB / API を引くのが明らかに無駄
  • 設定が 頻繁に変わらない
// ログイン時(例): session に「軽量な設定」だけ入れる
req.session.userUi = { lang: "ja", theme: "dark" };

問題点

  • session が太る(特に Redis store の場合、保存/取得が重くなる)
  • 設定変更時の session 更新漏れ(古い設定が残る)
  • 分散環境で地獄になりやすい(Redis必須、障害時の影響範囲も増える)

👉 「言語・テーマ・権限レベル」くらいならアリ
👉 「設定ファイル丸ごと」はやりすぎになりやすい

② 画面から毎回「設定取得API」を呼ぶパターン

今どき一番多い

特徴

  • SPA / SSR(Next等) / EJS等 どれでも自然
  • ステートレス寄りにできる(session を最小化できる)
  • キャッシュ戦略が組める
GET /api/user/settings

メリット

  • session を太らせない
  • 設定変更に追従しやすい(再取得すればよい)
  • スケールしやすい(セッション共有の依存が減る)

デメリット

  • 呼びすぎると無駄(同一ページ/同一操作で何度も呼ぶ等)

👉 なので普通は キャッシュとセットです。

③ サーバー側でキャッシュ(おすすめ)

業務系ではこれが一番バランスいい

Route(Controller)
 ↓
Service
 ↓
Cache(メモリ / Redis)
 ↓
DB
// 疑似コード例:ユーザー設定をキャッシュして返す
// ※ 実装は node-cache / lru-cache / Redis などで
async function getUserSettings(userId) {
  const cached = await cache.get(`userSettings:${userId}`);
  if (cached) return cached;

  const settings = await db.loadUserSettings(userId);
  await cache.set(`userSettings:${userId}`, settings, { ttlSeconds: 300 });
  return settings;
}
  • 画面は普通に API を呼ぶ
  • サーバー側でキャッシュして DB 負荷を抑える
  • session は最小限(認証情報など)

👉 「session=認証情報だけ」にしておくと設計が綺麗

実務でよくある“正解寄り”の構成

情報 置き場所
ログインID / userId session(or JWT の sub)
権限・ロール session(or JWT)
言語・テーマ session or cookie(軽量なら)
ユーザー設定(詳細) API + Cache
設定ファイル丸ごと ❌避ける(大きい/頻繁変更/画面依存なら特に)

判断基準(迷ったらこれ)

session に入れていいのは「毎リクエスト必須・小さい・変わりにくいもの」だけ。

それ以外は
設定取得API + サーバーキャッシュ
が Node.js でも「普通で怒られにくい」やり方です。