1. Visual Studio Codeで新規プロジェクトを作成する方法
Node.js は JavaScript を実行するランタイム(実行環境)。npm は Node.js 付属のパッケージ管理ツール。
1-1. VS Codeでフォルダーを開く
- 作業用フォルダーを作る(例:
C:\work\my-api)。 - VS Code → File → Open Folder... でそのフォルダーを開く。
- VS Code のターミナルを開く:Terminal → New 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": "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}`);
});
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:あなたが読む/書く。TypeScriptの世界。
- dist:機械が作る。Node.js が直接実行する JavaScript の世界。
- index.ts:起動点。「サーバーを立ち上げる」最初の1ファイル。
- routes:URLと処理の対応表を置く場所になりがち。
- services:HTTPと関係ない “本質の処理” を置く(テストもしやすい)。
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 build、main を dist/index.js に合わせる |
TypeError: (0 , express_1.default) is not a function |
import設定(Interop)が合っていない | tsconfig.json の esModuleInterop: true を確認 |
| 型エラーでビルドできない | strict が効いている | 型注釈・ガードを追加(後述) |
4. リリースビルド、配布
4-1. “配布物” とは何か
Node.js アプリは「単一exeに固めて配布」よりも、通常は以下の形になります。
- dist/(ビルド成果物の JavaScript)
- package.json(依存と起動コマンド)
- package-lock.json(依存の固定。再現性のため重要)
「Node.js を同梱して単体配布」もできますが(pkg/nexe 等)、まずは標準的な配布から始めるのが学習コストが低いです。
4-2. リリース手順(標準)
- 開発PCでビルド
npm run clean npm run build - 配布先フォルダーへファイルをコピー(例)
dist/ package.json package-lock.json - 配布先で依存をインストール
npm ci --omit=devnpm ci:lockファイル(package-lock.json)どおりに機械的にインストールするコマンド。再現性が高い。
--omit=dev:devDependenciesを入れない(本番に不要なものを省く)。 - 起動
npm start
4-3. 環境変数でポートなどを切り替える
環境変数 は、コードを書き換えずに設定を変える仕組みです(例:ポート、DB接続先、APIキー)。
# Windows PowerShell
$env:PORT="8080"
npm start
# macOS / Linux
PORT=8080 npm start
4-4. 配布の形の例
- 社内配布(zip):上記ファイルをzip → 配布先で
npm ci→npm start - サーバー配置:dist + package*.json を配置 → CI/CD で
npm ci→ PM2等で常駐 - Docker:Dockerfileで build → イメージ配布(再現性が高い)
「Windowsサービス化」「PM2」「Docker」などは次の章として分けた方が読みやすいです。
5. TypeScriptの型、文法(if, switch, while, for, foreach など)
つまり「型で事故を減らす」ために書きます。
5-1. 基本の型
| 型 | 例 | 意味 |
|---|---|---|
string | const s: string = "abc" | 文字列 |
number | const n: number = 123 | 数値(整数/小数の区別なし) |
boolean | const b: boolean = true | true/false |
null / undefined | let x: string | null = null | 値がない状態(2種類) |
any | let v: any = ... | 型チェック放棄(最後の手段) |
unknown | let v: unknown | 不明な値(使う前に絞り込みが必要) |
void | function f(): void {} | 戻り値を返さない |
never | function 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" };
実行時には存在せず、
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(抽象クラス) を すべて使うことができます。 ただし、それぞれは 役割がまったく異なる ため、 使い分けがとても重要です。
① 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の入出力・DTO | interface / 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 には、C / C++ / C# にあるような 「struct」という専用の言語機能はありません。
ただしこれは「できない」という意味ではなく、 TypeScript では struct と同じ目的を、別の書き方で実現する という設計になっています。
そもそも「struct が欲しい」とは、何をしたいのか?
初心者が「struct はないのですか?」と感じる場面は、だいたい次の用途です。
- 複数の値を 1 つのまとまりとして扱いたい
- クラスほど重い仕組みは使いたくない
- メソッドやロジックは不要
- API や DB のデータ構造を表したい
① interface:もっとも基本的な「struct 相当」
// データの「形」だけを定義
interface User {
id: number;
name: string;
email?: string; // あってもなくてもよい
}
// 普通の JavaScript オブジェクト
const u: User = {
id: 1,
name: "mao"
};
この User は次の特徴を持ちます。
- 実行時には存在しない
- new できない
- メモリを持たない
- 型チェックのためだけに使われる
② 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; ❌ コンパイルエラー
一度作ったら変更できないため、
- 座標
- 金額
- 設定値
readonly struct に近い考え方です。
④ as const:定数としての構造体
const Status = {
Pending: "pending",
Paid: "paid",
Canceled: "canceled"
} as const;
これは:
- 実行時は普通のオブジェクト
- 型的には値が固定される
⑤ class:これは「構造体」ではない
class User {
constructor(
public id: number,
public name: string
) {}
greet() {
return `hello ${this.name}`;
}
}
const u = new User(1, "mao");
class は:
- 実行時に存在する
- new される
- メソッド(振る舞い)を持つ
⑥ 実務での定番パターン
// 構造体相当(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
5-4. Union(合併型)と型ガード(絞り込み)
type Input = string | number;
function toNumber(x: Input): number {
if (typeof x === "string") {
return Number(x);
}
return x; // ここでは number と分かっている
}
外部入力(HTTP body など)は基本 “信用しない” →
unknown で受け → 条件で絞る、が堅い書き方です。
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
型ガード(型の絞り込み)とは何か
型ガードとは、
Union 型の値について 「今この場所では、どの型なのか」を確定させる処理のことです。 一番基本的な型ガードは
typeof です。
typeof x === "string"
これは:
- 実行時には JavaScript の条件判定
- コンパイル時には TypeScript へのヒント
型ガードがあると、何が変わるのか
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);
});
途中で抜けたい(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 で受け取る値の型がどう扱われるか」です。
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");
}
「失敗したときにどうするか」を 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");
}
}
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 });
});
HTTPで返す:APIとして「失敗をレスポンスで表現」したい(400/404/500 など)
5-10. 非同期処理(Promise / async / await)— Node.jsで最重要
Node.js では、HTTP・ファイル・DB などの I/O(入出力)は 非同期 が基本です。
理由は単純で、I/O は「待ち時間(通信やディスク待ち)」が長く、そこで処理を止めると
1つのプロセスが何もできない時間が増え、同時にさばけるリクエスト数が減ってしまうからです。
非同期(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 で例えると、ざっくり次のどれかに近いです(完全一致ではありません)。
- Future / CompletableFuture:未来の結果を保持する(かなり近い)
- Callback(コールバック):完了時に呼ばれる関数を渡す(Promiseの内部はこれを整理したもの)
- Coroutine(Kotlin):awaitの見た目は coroutine にかなり近い
ただし 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);
});
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();
awaitは async 関数の中でしか使えない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),
]);
依存がない場合は
Promise.all で並列化すると速くなります。
9) Android Java と比べて何が違うのか(感覚を揃える)
-
スレッドを自分で作る感覚が薄い
Android Java だと Thread / Executor / Handler などを意識しますが、Node はイベントループ中心です。
ただし「非同期=並列にCPUで実行」ではなく、「待ち時間の間に他の処理を進める」が基本です。 -
戻り値が Future ではなく Promise
役割は似ていますが、書き方の主流がasync/awaitになっている点が大きな違いです。 -
例外の扱い
Java の checked exception のような仕組みはありません。
しかしawaitで reject は例外になるので、必要ならtry/catchで捕まえます。
10) ありがちな落とし穴(初心者が最初に踏む)
-
await を付け忘れる
const data = fetchJson(url)は data が Promise になり、値ではありません。 -
forEach で await しようとする
array.forEach(async ...)は待ち合わせできません。必要ならfor...ofかPromise.allを使います。 -
エラー処理をしない
awaitは例外を投げるので、外部I/Oでは必ず失敗パスを考えます。
Promise<T> になる。async/await を使うと、同期処理に近い形で安全に書ける。失敗(reject)は
await で例外になり、必要なら try/catch で扱う。
サンプル:/user/get/all → 他サーバーGET → JSON解析 → 必要項目だけ返す
この節でやること(概要):
- Express の Router で
GET /user/get/allを受ける - Router から Controller を呼ぶ
- Controller の中で「他サーバーへ HTTP GET」して JSON を受け取る
- 受け取った JSON の中から
userid,username,emailだけを抜き出す - クライアントへ
id,name,emailの一覧を JSON で返す - 同じ処理を async/await 版と .then().catch() 版で見比べる
{
"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 つを 連結 して作られます。
- app.use() で指定した「ベースパス」
- 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 内部では、次の順で処理されます:
-
app レベル
「URL が/userで始まっているか?」
→ YES →userRouterに処理を委譲 -
Router レベル
Router に渡る時点で URL は/get/allになる -
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() で指定したパスの続き」を書く。
だから:
この理解ができると、Express の URL 設計で迷わなくなります。app.use("/user", userRouter)
+
userRouter.get("/get/all")
=
/user/get/all
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}`);
});
const res = await fetch(url, { method: "GET" });
この await は、HTTP 通信が「完全に終わるまで」待っているわけではありません。
結論から正確に言うと
await fetch(...) が待っているのは、
HTTP リクエストを送信し、です。
サーバーから レスポンスヘッダ(ステータスコードやヘッダ情報) が返ってくるまで
レスポンスの本文(JSONやテキスト)は、まだ受信し終わっていません。
HTTP 通信は内部的に 2 段階で進む
- リクエスト送信 → レスポンスヘッダ受信
- レスポンス本文(ボディ)を最後まで読む
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();
これは:
- スレッドをブロックする
- ヘッダも本文もまとめて待つ
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);
});
}
try {
const external = await fetchExternalUsers(...);
res.json(...);
} catch (err) {
next(err);
}
then/catch 版
fetchExternalUsers(...)
.then((external) => {
res.json(...);
})
.catch((err) => {
next(err);
});
await X=X.then(...)(成功時の処理)try/catch=.catch(...)(失敗時の処理)throw new Error(...)= Promise で言うとreject(失敗として伝える)
実務で「Router/Controller/Service」を分ける理由(要約ではなく理由を明確に)
-
Router:URL と処理(Controller)を結びつけるだけにする。
ルーティングが増えても見通しが良い。 -
Controller:HTTP の世界(req/res)を扱う場所。
「入力を受ける」「サービスを呼ぶ」「レスポンスを返す」に集中する。 -
Service:外部API/DB/ファイルなどの I/O をまとめる場所。
Controller から切り離すとテストや再利用がしやすい。
このサンプルの入出力(何が返るのか)
他サーバーから受け取る 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" }
]
}
unknown で受けて、型ガード(検証)してから扱うのが堅いです。ここは学習段階として「最低限の形チェック」だけ入れています。
6. ファイルI/O と DBアクセス(SQLite3)— Node.js/Express/TypeScript 実務入門
この章では、Node.js バックエンドで避けて通れない ファイルI/O と DBアクセスを、 初心者でも「何を・なぜ・どう書くか」が分かるように、コードを多めに示しながら説明します。
6-1. ファイルI/O(File Input/Output)とは
ファイルI/O とは「ファイルを読む/書く」ことです。Node.js ではディスクアクセスは遅い(待ち時間がある)ため、 基本的に 非同期 で扱います。
Promise版API:
fs/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文字列に値を安全に埋め込む仕組み(SQL注入対策)。
トランザクション:複数の更新を「全部成功 or 全部失敗」にまとめる仕組み(整合性)。
コネクション:DBへの接続。SQLiteは「ファイルを開く」ことに近い。
コネクションプール:接続を使い回して高速化する仕組み(SQLiteでは通常不要/概念が違う)。
7-1. 使用ライブラリ方針(初心者向けのおすすめ)
SQLite を Node.js から使う方法はいくつかあります。学習のしやすさ重視で以下を採用します。
- sqlite3(公式に近い定番)+ sqlite(Promiseラッパー)
これにより 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;
}
「起動時に開く → 使い回す」が分かりやすく、トラブルも少ないです。
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;
}
"... 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 した 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:アプリ起動時に 1つ開いて使い回す(この章の設計)
- サーバーDB:プールを作って使い回す(後述)
「ローカル用途」「小〜中規模」「配布が簡単」が強みです。
8. どのDBに接続するか・プール・主要DBの違い
8-1. DBを選ぶ観点(初心者向け)
- 配布の簡単さ:SQLite(DBファイル1個)
- 同時アクセス/高負荷:PostgreSQL / MySQL / SQL Server / Oracle(サーバーDB)
- 運用(バックアップ/監視/冗長化):サーバーDBが強い
- クラウド連携:PostgreSQL/MySQL は選択肢が多い
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;
}
プールは接続を使い回すことで、性能と安定性を上げます。
8-3. Oracle / MySQL / PostgreSQL / SQL Server の違い(初心者向けまとめ)
| DB | 特徴 | 注意/相性 |
|---|---|---|
| PostgreSQL | 機能が強い・拡張性が高い・オープンソースで人気 | SQLが厳密寄り。JSON型や拡張が便利 |
| MySQL | 採用例が多い・運用ノウハウが豊富 | ストレージエンジン等の理解が必要になることがある |
| SQL Server | Microsoft環境で強い・企業で多い | T-SQL(方言)やWindows運用の文化がある |
| Oracle | 大規模・堅牢・企業基幹で強い | ライセンス/運用が重め。方言も多い |
例:
LIMIT/OFFSET の扱い、日付関数、UPSERT構文など。「DBをまたいで動くSQL」を書きたい場合は、後述の ORM/Query Builder が効きます。
9. SQL以外でアクセスできる?(Spring Boot の @Entity 的なもの)
はい、できます。Node/TypeScript の世界では、 ORM(Object-Relational Mapping)や Query Builder を使うと、 SQLを直接書かず(または最小限で)DB操作できます。
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に対応したい」場合の候補になります。
- 学習や小規模:SQL直書き(この章のSQLite方式)は理解が早い
- 中〜大規模:ORM/Query Builder は保守性・型安全・移植性で効く
- 複雑なSQL:結局SQLが必要な場面もある(ORMでも生SQLを許可することが多い)
10. 章のまとめ
- ファイルI/Oは
fs/promisesが基本。大きいファイルはストリーム。 - SQLiteは「DBファイル」で配布が簡単。起動時に開いて使い回す設計が分かりやすい。
- サーバーDB(Oracle/MySQL/PostgreSQL/SQL Server)はプールが重要。
- SQLを減らしたいならORM/Query Builder(TypeORM/Prisma等)が選択肢。
SQLite は「軽量で手軽」なデータベースですが、
同時アクセス(並行処理)については
RDBMS(MySQL / PostgreSQL など)とは考え方が違います。
1. SQLite は「ファイルを直接ロックするDB」
SQLite はサーバープロセスを持たず、 1つの DB ファイル(.sqlite / .db)を直接読み書きします。
- DB = 1ファイル
- OSのファイルロックを使用
- ネットワーク越しの多人数利用は想定外
そのため、同時に複数の書き込みが発生すると、 ロック競合が起きやすくなります。
2. デフォルト(ROLLBACK JOURNAL)モードの問題点
SQLite の初期設定は ROLLBACK JOURNAL モードです。
このモードでは:
- 書き込み時に DB 全体をロック
- その間、読み取りもブロックされる
- Node.js の並行リクエストと相性が悪い
// 悪い例(同時アクセス時)
Request A: INSERT → 書き込みロック取得
Request B: SELECT → 「database is locked」
Web API では、
「書き込み中に read が来る」のは日常茶飯事なので、
この挙動は致命的です。
3. WAL(Write-Ahead Logging)モードとは?
そこで使うのが WAL モード です。
WAL は名前の通り:
「まずログに書いてから、あとでまとめて本体に反映する」
仕組みを簡単に言うと:
- 書き込み → WALファイル(別ファイル)に追記
- 読み取り → 本体DB + WALを参照
- 読み取りと書き込みが同時に可能
DB本体: app.db
WALログ: app.db-wal
共有メモリ: app.db-shm
4. WAL モードの最大のメリット
- ✅ 読み取り中でも書き込み可能
- ✅ Web API の同時リクエストに耐えやすい
- ✅ Node.js / Express と相性が良い
実務で SQLite を使うなら、
WAL モードは「ほぼ必須」です。
5. WAL モードの設定方法(必ず最初に実行)
// 起動時に1回だけ実行する
db.exec("PRAGMA journal_mode = WAL;");
db.exec("PRAGMA synchronous = NORMAL;");
journal_mode = WAL:WAL有効化synchronous = NORMAL:性能と安全性のバランス
途中で切り替えると効果が分かりにくくなります。
6. WAL でも「書き込みは1つだけ」
誤解しやすい点ですが:
WAL = 書き込みが無限に並列になる ではありません。
- 同時 READ:複数OK
- 同時 WRITE:1つだけ
つまり:
- READが多いAPI → 向いている
- WRITEが多い高負荷API → 向いていない
7. Node.js でよくある「database is locked」の原因
- WAL を有効にしていない
- トランザクションを長く保持している
- 同期処理(重い処理)をトランザクション内で実行
- 大量 INSERT を1件ずつ実行している
// 悪い例
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:ファイルロック
- MySQL / PostgreSQL:行ロック / MVCC
- SQLite:同時WRITEは1つ
- PostgreSQL:同時WRITE多数OK
まとめ(重要)
SQLite は「軽くて便利」ですが、 同時アクセスには制約があるDBです。
Webアプリで使うなら:
- WAL モードを必ず有効化
- トランザクションを短く保つ
- 書き込みが増えたら別DBを検討
「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 に書き込む処理は、
必ず 前の書き込みが終わってから 実行する」
- READ:並列でOK
- WRITE:順番に1つずつ
これは 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();
}
);
});
});
ポイント:
- DB に触るのは
enqueueWriteの中だけ - 外から直接
db.run()しない
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 });
}
複数リクエストが同時に来ても:
- 1件目が完了
- 2件目が開始
- 3件目が開始
という順序が保証されます。
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() - ❌ トランザクションをまたいで await
- ❌ heavy 処理を BEGIN〜COMMIT の中で実行
- ❌ READ と WRITE を同じキューに入れる
// 悪い例
db.run("BEGIN");
await heavyLogic(); // ← ロック保持
db.run("COMMIT");
8. better-sqlite3 を使う場合(補足)
better-sqlite3 は同期 API のため、
JS の実行順 = 書き込み順になります。
そのため:
- 1プロセス
- 1スレッド
の場合は、暗黙的にキュー化されます。 ただし:
- CPU重い処理があると全体停止
- Worker Threads では別途設計が必要
9. SQLite キューイングの限界
- 書き込みスループットは上がらない
- 高頻度 WRITE には不向き
- 「詰まり始めたら」別DB検討のサイン
10. 他DBとの考え方の違い
| DB | 書き込み並列 | キュー必要? |
|---|---|---|
| SQLite | 1つ | 必須 |
| MySQL | 複数 | 不要 |
| PostgreSQL | 複数 | 不要 |
| Oracle | 複数 | 不要 |
まとめ(重要)
SQLite を Web API で使うなら:
- WAL モードを有効化
- WRITE を必ずキューに通す
- トランザクションは短く
- 詰まり始めたら DB 移行を検討
SQLite は「小規模・低〜中頻度 WRITE」では 非常に優秀な DBです。 キューイングは、その性能を引き出すための 必須テクニックです。
結論から言うと、
Node.js を使えば、Windows / macOS / Linux の違いを意識せずに
テンポラリーファイルを安全に利用できます。
ただし、それは 「正しい API を使った場合に限る」という前提があります。
1. OS ごとの「一時ディレクトリ」の違い
各 OS には「一時ファイルを置く標準ディレクトリ」があります。
| OS | 代表的な一時ディレクトリ |
|---|---|
| Windows | C:\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() は:
- Windows / macOS / Linux を自動判別
- その環境で「正しい」一時ディレクトリを返す
- Docker / CI / サーバー環境でも安全
👉 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);
この方法のメリット:
- OS非依存
- 名前衝突しない
- 後片付けが簡単(ディレクトリごと削除)
4. 大きなファイルでは「ストリーム」が基本
大容量ファイル(画像・動画)では、 メモリに全部載せてはいけません。
import fs from "fs";
import path from "path";
const writeStream = fs.createWriteStream(tmpFile);
req.pipe(writeStream);
writeStream.on("finish", () => {
console.log("一時保存完了");
});
この方法は:
- メモリ消費が少ない
- OS差分なし
- 巨大ファイルでも安全
5. 後片付け(重要)
テンポラリーファイルは 必ず削除する設計にします。
// ディレクトリごと削除
await fs.rm(tmpDir, { recursive: true, force: true });
削除漏れがあると:
- ディスクが埋まる
- サーバーが突然落ちる
- 調査が地獄になる
6. Express + multer との関係
multer は内部的に:
os.tmpdir()を使う- 一時ファイル or メモリに保存
つまり、multer を正しく使えば OS差分はすでに吸収されています。
memoryStorage より
diskStorage + テンポラリーディレクトリが安全です。
7. コンテナ(Docker)環境での注意
- コンテナ内の
/tmpは揮発性 - 再起動で消える(=テンポラリ用途として正しい)
- 永続化したい場合は volume を使う
8. 他言語(Java / Spring Boot)との対応
| 言語 | 一時ディレクトリ取得 |
|---|---|
| Node.js | os.tmpdir() |
| Java | System.getProperty("java.io.tmpdir") |
| Python | tempfile.gettempdir() |
考え方は すべて同じです。
まとめ(重要)
- OS差分は
os.tmpdir()が吸収する - 一時ファイルは 一意なディレクトリに置く
- 大きいファイルはストリームで扱う
- 後片付けを必ず行う
これを守れば、 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 では メモリに丸ごと載せない(ストリーミング)設計が重要です。
ストリーミング:データを少しずつ読み書きする(巨大ファイルでもメモリ爆発しない)。
multer:Expressでファイルアップロードを扱う定番ミドルウェア(内部でストリーム処理)。
Content-Disposition:ダウンロード時にファイル名を指定するためのヘッダ。
11-1. 方針(大きいファイルで重要なこと)
-
アップロードはメモリに載せない:
memoryStorageは避け、disk保存する - サイズ制限を必ず付ける:無制限だとDoS(大容量連打)で落ちる
- 拡張子を信用しない:最低限 mime-type を見る(より厳密には「内容判定」)
-
ダウンロードはストリームで返す:
readFileで丸読みしない
11-2. 事前準備(ライブラリ)
# 画像アップロード処理に multer を使う
npm i multer
npm i -D @types/multer
11-3. まず Router から(/file/upload, /file/download/:id)
ルーティングは次の2つを用意します:
POST /file/upload:HTMLフォームから画像を受け取って保存GET /file/download/:id:保存したファイルをダウンロード(または画像表示)
// 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);
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制限を入れるのが第一歩です。
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 はファイル全体をメモリに読みます。大きい画像だとメモリが危険です。ここでは
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:流れを組み立てる / 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 の fetch と FormData を使うと便利です。
<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>
fetch だけだと「アップロード進捗」を取りにくいです。進捗が必要なら
XMLHttpRequest を使うか、より新しいAPI/ライブラリで対応します(実務話)。
11-8. 大きいファイルで必須の安全策(実務の現実)
- サイズ制限:
limits.fileSize - 拡張子/ MIME の制限:
fileFilter - 保存先の固定:ユーザー入力で保存パスを作らない(パストラバーサル対策)
- ファイル名のランダム化:衝突防止 + 悪意ある名前対策
- メタ情報の保存:本当は「id → filename / mime / size」をDBに保存する(次節で強化可能)
実務では S3 などのオブジェクトストレージに直接アップロードさせる設計もよくあります。
11-9. ここまでの動作確認(手順)
- サーバー起動(例:
npm run dev) - ブラウザでアップロードフォームを開く
- 画像を選択して送信 → JSONで
idとdownloadUrlが返る 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);
YESならルーターを分けます。
2. Controller / Service / Repository の分け方
最初から完璧に分ける必要はありません。
「HTTPの話」と「業務処理」が混ざり始めたら分離が合図です。
src/
controllers/
user.controller.ts
services/
user.service.ts
repositories/
user.repository.ts
- Controller:req / res を扱う(HTTPの世界)
- Service:業務ロジック(ルール・判断)
- Repository:DBアクセスのみ
「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" });
});
詳細はレスポンスに出さずログに出します。
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;
- .env は
.gitignoreに入れる - 本番では OS / Kubernetes / CI の環境変数を使う
6. 最低限のセキュリティ
- CORS:どのサイトから呼べるか制御
- helmet:安全な HTTP ヘッダを自動付与
- rate limit:総当たり攻撃対策
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. ファイルは分けるのが普通か?
結論から言うと:
よくある構成例
src/
models/
user.ts // interface User
entities/
user.entity.ts // class UserEntity
enums/
user-role.ts // enum UserRole
services/
user.service.ts // class UserService
理由はシンプルです:
- ファイル名=中身が分かる
- import が明確になる
- 差分(git)が追いやすい
- 肥大化を防げる
関連する 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 | クラス内部のみ |
4. Java / C# との違い(重要)
TypeScript のアクセス修飾子は:
つまり:
- コンパイル時:アクセス違反はエラー
- 実行時:理論上は触れてしまう
それでも使う理由は:
- 設計意図を明確にする
- IDE 補完を制御できる
- 誤用を早期に防げる
5. 初心者向けおすすめルール
- class / enum は 1ファイル1定義
- interface は「公開用」だけ分離
- 内部 type は同一ファイル
- メソッドは原則 private
- 外に出す最小限だけ public
このルールを守ると:
- ファイル構成で迷わない
- 依存関係が破綻しにくい
- 後からリファクタしやすい
TypeScript では、
「何を公開するか」と
「どこに置くか」を
設計で決めるのが重要です。
言語が自由だからこそ、
ルールを自分で作ると初心者でも破綻しません。
Node.js に DI(Dependency Injection)はあるのか?
結論から言うと:
代わりに「設計としての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です。
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) {}
}
これにより:
- SQLite → PostgreSQL に差し替え可能
- テスト用の Mock に差し替え可能
4. Node.js に DI コンテナはある?
あります。ただし 必須ではありません。
| ライブラリ | 特徴 |
|---|---|
| tsyringe | 軽量・初心者向け |
| InversifyJS | Spring風・高機能 |
| NestJS | フレームワーク一体型DI |
「なぜ動くのか分からないコード」になります。
5. Spring Boot との違い
| Spring Boot | Node.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();
Java や C# のように実行時に強制されるわけではありません。
そのため、クラス自体を public にする意味は薄いです。
例外処理(Exception Handling)
Node.js + Express + TypeScript では、 「どこで例外を投げ、どこで HTTP エラーにするか」 を決めておかないと、コードがすぐに壊れます。
このセクションでは、実務で最も一般的な考え方として:
- Controller / Service / Repository の責務分担
- Express の error middleware による一括処理
- HTTP エラー専用の型
HttpError
を 順番に説明します。
1. なぜ例外処理を決める必要があるのか
初心者が書きがちなコードでは、次のような問題が起きます。
- どこかで
throwされた例外が突然 500 になる - 同じエラー処理が Controller ごとにコピペされる
- ログが出たり出なかったりする
原因はシンプルで:
「誰が、何の責任で例外を処理するのか」 が決まっていない
そこで、レイヤーごとに 責務を明確に分けます。
2. レイヤーごとの例外責務
| レイヤー | 役割 | 例外の扱い |
|---|---|---|
| Repository | DB / 外部I/O |
技術的な例外を そのまま throw (DBエラー、接続失敗など) |
| Service | 業務ロジック |
意味のある失敗を 例外として表現 (NotFound / Validation など) |
| Controller | HTTP 層 |
原則 例外を処理しない (投げて middleware に任せる) |
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;
}
}
これにより、
- 「これは 404」
- 「これは 400」
を 例外そのものに意味として持たせることができます。
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;
}
}
ここでは:
- HTTP レスポンスは返していない
- 「見つからない」という意味だけを表現
という点が重要です。
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" });
});
ログは必ず出すのが実務の基本です。
7. 例外処理の全体像
- Repository が技術的エラーを throw
- Service が意味のある失敗を HttpError として throw
- Controller は例外を触らない
- error middleware が 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を題材に、
- DB(SQLite)
- ファイル I/O(画像アップロード)
- 外部 HTTP API
2. エンドポイント仕様
| Method | Path | 概要 |
|---|---|---|
| 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 の文法を業務ロジックの中でどう使うかに焦点を当てています。
const/letの使い分け- 基本型(
number,string,boolean) interface/typeenum(業務上の区分)for...ofによる配列処理switchによる条件分岐map/reduceによる集計
「文法の説明」ではなく、 実際の業務ルールの中で文法がどう使われるか を理解するのが目的です。
2. 業務の概要
ユーザーから渡された「注文データ」を元に、 以下を計算します。
- 商品ごとの金額と税額
- 顧客ランクやクーポンによる割引
- 配送地域による送料
- 最終的な請求金額
3. 入力データの考え方
入力は JavaScript オブジェクト(JSON 相当)です。 実務では HTTP body に相当します。
重要なポイント:
- 配列(注文行)を
for...ofで処理する - 区分値は
enumで表現する - 計算途中の値は
constで保持する
4. 主な業務ルール
税計算
- STANDARD:10%
- REDUCED:8%
- EXEMPT:0%
割引
- GOLD 会員:小計の 5%
- WELCOME10 クーポン:小計の 10%
送料
- TOKYO:500円
- OTHER:800円
- 小計 10,000円以上で送料無料
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で処理するcustomerRankやtaxは enum で安全に扱うcouponは あってもなくてもよい(optional)
出力(計算結果)
業務ロジックが返す計算結果です。
API のレスポンスや画面表示用データにそのまま使えます。
// 計算結果(出力)
{
"subtotal": 3460, // 税抜小計(全行の subtotal 合計)
"taxTotal": 292, // 税額合計
"discount": 346, // 割引合計(会員 + クーポン)
"shipping": 500, // 送料
"grandTotal": 3906 // 最終請求金額
}
- 各行の
subtotalを計算 - 税区分ごとに税額を算出
- 小計を
reduceで集計 - 割引・送料を適用
- 最終合計を算出
実際の 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];
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 は「参照の再代入禁止」であり、中身の変更は禁止していません。
2-2. よく使う配列メソッド
| メソッド | 用途 |
|---|---|
| push | 末尾に追加 |
| pop | 末尾を削除 |
| map | 変換 |
| filter | 絞り込み |
| reduce | 集計 |
| find | 1件検索 |
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"
オブジェクトとの違い
- キーに
numberやobjectを使える - 順序が保証される
- サイズ取得が簡単(
map.size)
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. まとめ
- 配列は List の代わりで、サイズ変更可能
- Map はキー付きデータ、Set は重複防止
- string / number にも豊富なメソッドがある
- 正規表現で文字列を構造的に扱える
TypeScript の文法は、 データを安全に・読みやすく扱うための道具です。
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"
実務では
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 }
特に「途中で 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(不変)」とは、元のデータを直接書き換えず、 新しいデータを作って返す書き方です。
なぜ重要か:
- 副作用(意図しない書き換え)が減る
- バグの原因を追いやすい
- React などのUIフレームワークと相性が良い
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. forEach で return しても止まらない
初心者が混乱しやすい点です。forEach の return は
「コールバックから戻る」だけで、ループを止めません。
// ❌ 止まらない
[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 は元配列を破壊する
sort は in-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. まずは配列で十分なケース
- 要素数が小さい(数十〜数百)
- 順番に処理するだけ(
for...of) - 検索がたまにしか発生しない
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(内容が同じでも別オブジェクト)
- 順番に処理 → Array(for...of / map / filter / reduce)
- idで高速検索 → Map
- 重複排除・存在チェック → Set
11. string / number メソッドの実務的まとめ(追加)
「一覧」と言うと膨大になるため、実務で頻出のものを目的別に整理します。 ここに載っていないメソッドもありますが、まずはこれで十分です。
11-1. string(検索・切り出し・整形)
| 目的 | メソッド | 例 |
|---|---|---|
| 長さ | length | "abc".length |
| 部分一致 | includes | s.includes("x") |
| 前方/後方一致 | startsWith, endsWith | s.startsWith("http") |
| 切り出し | slice, substring | s.slice(0, 3) |
| 分割 | split | s.split(",") |
| 置換 | replace, replaceAll | s.replace("a","b") |
| 空白処理 | trim, trimStart, trimEnd | s.trim() |
| 大小文字 | toLowerCase, toUpperCase | s.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);
}
- 入力検証は「正規表現だけ」に頼りすぎない(読みづらくなる)
- ログ解析・パターン抽出には強い
- 名前付きグループを使うと保守が楽
例えば「ユーザーの画面設定(テーマ・言語など)を、ログイン時に DB から取ってきて session に入れておく」のは、Node.js(Express)では普通でしょうか?
それとも、画面から毎回「設定取得API」を呼ぶのが普通でしょうか?
「ユーザー設定ファイルを丸ごと 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 でも「普通で怒られにくい」やり方です。