0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScript基礎からHonoでTodo APIを作るまで

0
Posted at

はじめに

JavaScriptは触ったことあるけどTypeScriptは軽くしか触ってない、という人向けに書く。

この記事では:

  1. TypeScriptの基礎(JSと違うところ中心)
  2. Honoのセットアップ
  3. Todo APIの実装

ここまでを一本で通す。


1. セットアップ

Node.jsがインストール済み前提で進める。

mkdir todo-hono
cd todo-hono
npm init -y
npm install typescript tsx --save-dev
npx tsc --init

tsconfig.json が生成される。今回はデフォルトのままでOK。

動作確認用に index.ts を作る。エディタ(VSCodeなど)で新規ファイルとして作成し、以下を書いて保存する。

const message: string = "Hello, TypeScript!";
console.log(message);
npx tsx index.ts
Hello, TypeScript!

tsx はTypeScriptをコンパイルなしで直接実行できるツール。開発中はこれで十分。

Windows(PowerShell)で echo "..." > index.ts のようにリダイレクトでファイルを作ると、既定のエンコーディングがUTF-16やShift-JISになり、esbuild(tsx が内部で使っている)が文字化けとして読めず Unexpected "�" のようなエラーになることがある。エディタで直接ファイルを作成するか、PowerShellで作る場合は Set-Content -Encoding utf8NoBOM を使ってUTF-8(BOMなし)を明示する。


2. TypeScript基礎

型注釈

let age: number = 30;
let name: string = "Alice";
let isActive: boolean = true;

// 型推論が効くので明示しなくてもいい場合が多い
let count = 0; // number と推論される

JavaScriptとの一番の違いは、変数・引数・戻り値に「型」を書けること。書かなくても動くコードもあるが、型があることでバグをコンパイル時に検出できる。

関数の型

function add(a: number, b: number): number {
  return a + b;
}

// アロー関数でも同様
const multiply = (a: number, b: number): number => a * b;

引数の型を間違えると、実行前(コンパイル時)にエラーになる。

add("1", "2"); // Argument of type 'string' is not assignable to parameter of type 'number'

インターフェース

オブジェクトの形を定義する。

interface Todo {
  id: number;
  title: string;
  done: boolean;
}

const todo: Todo = {
  id: 1,
  title: "買い物",
  done: false,
};

必須プロパティが足りないとコンパイルエラーになる。? をつけるとオプショナル(任意)になる。

interface Todo {
  id: number;
  title: string;
  done: boolean;
  memo?: string; // あってもなくてもいい
}

型エイリアス

type でも似たようなことができる。

type Status = "pending" | "done" | "archived";

function updateStatus(status: Status) {
  console.log(status);
}

updateStatus("pending"); // OK
updateStatus("invalid"); // エラー:Status型にない値

このような「決まった文字列のどれか」という型をUnion型(合併型)と呼ぶ。Todoの完了状態などに使うと、タイプミスをコンパイル時に防げる。

ジェネリクス

型を後から指定できる「型のテンプレート」。

function wrapInArray<T>(value: T): T[] {
  return [value];
}

wrapInArray(1);        // number[]
wrapInArray("hello");  // string[]
wrapInArray(true);     // boolean[]

<T> の部分が「呼び出し時に決まる型」を表す。配列や後述のAPIレスポンスの型でよく使う。


スクリーンショット 2026-08-02 131131.png

3. Honoのセットアップ

Honoを追加する。

npm install hono
npm install @hono/node-server

src/index.ts を作る:

import { Hono } from "hono";
import { serve } from "@hono/node-server";

const app = new Hono();

app.get("/", (c) => c.text("Hello, Hono!"));

serve({
  fetch: app.fetch,
  port: 3000,
});

console.log("サーバー起動: http://localhost:3000");

実行:

npx tsx src/index.ts
curl http://localhost:3000
# Hello, Hono!

4. Todo APIを実装する

データの型を定義する

src/types.ts:

export interface Todo {
  id: number;
  title: string;
  done: boolean;
}

export interface CreateTodoBody {
  title: string;
}

インメモリストアを作る

src/store.ts:

import type { Todo } from "./types";

let todos: Todo[] = [];
let nextId = 1;

export const todoStore = {
  getAll(): Todo[] {
    return todos;
  },

  add(title: string): Todo {
    const todo: Todo = { id: nextId++, title, done: false };
    todos.push(todo);
    return todo;
  },

  markDone(id: number): Todo | undefined {
    const todo = todos.find((t) => t.id === id);
    if (todo) {
      todo.done = true;
    }
    return todo;
  },
};

Todo | undefined という書き方もUnion型。「見つかればTodo、見つからなければundefined」という戻り値を型で表現している。

ルーティングを実装する

src/index.ts を以下で置き換える:

import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { todoStore } from "./store";
import type { CreateTodoBody } from "./types";

const app = new Hono();

// 一覧取得
app.get("/todos", (c) => {
  return c.json(todoStore.getAll());
});

// 追加
app.post("/todos", async (c) => {
  const body = await c.req.json<CreateTodoBody>();

  if (!body.title || body.title.trim() === "") {
    return c.json({ error: "title is required" }, 400);
  }

  const todo = todoStore.add(body.title);
  return c.json(todo, 201);
});

// 完了にする
app.put("/todos/:id/done", (c) => {
  const id = Number(c.req.param("id"));
  const todo = todoStore.markDone(id);

  if (!todo) {
    return c.json({ error: "todo not found" }, 404);
  }

  return c.json(todo);
});

serve({
  fetch: app.fetch,
  port: 3000,
});

console.log("サーバー起動: http://localhost:3000");

c.req.json<CreateTodoBody>() のようにジェネリクスで型を指定すると、リクエストボディに型がつく。基礎で説明したジェネリクスがここで実際に使われている。


5. 動作確認

npx tsx src/index.ts

Todo追加

curl -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "買い物"}'
{"id":1,"title":"買い物","done":false}

一覧取得

curl http://localhost:3000/todos
[{"id":1,"title":"買い物","done":false}]

完了にする

curl -X PUT http://localhost:3000/todos/1/done
{"id":1,"title":"買い物","done":true}

バリデーション確認

curl -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title": ""}'
{"error":"title is required"}

スクリーンショット 2026-08-02 131225.png

TypeScriptの型がAPIでどう役立つか

箇所 型の役割
Todo インターフェース レスポンスの形を保証する
CreateTodoBody リクエストボディの形を明示する
Todo | undefined 「見つからない場合」をコンパイル時に意識させる
c.req.json<CreateTodoBody>() パースしたJSONに型をつける

JavaScriptだと body.titel(タイポ)のようなミスは実行時までわからないが、TypeScriptなら書いた瞬間にエディタが赤線で教えてくれる。特にAPIのリクエスト/レスポンスの形が変わりやすいプロジェクトほど、型の恩恵は大きい。


スクリーンショット 2026-08-02 131247.png

まとめ

要素 役割
interface オブジェクトの形を定義
Union型(| 複数の型のどれか、または「値かundefinedか」を表現
ジェネリクス(<T> 型を後から指定できるテンプレート
Hono 軽量・型安全なWebフレームワーク

Express経験者からすると、Honoは書き味が近いまま型の恩恵を強く受けられるのが特徴。c.req.json<T>() のようにフレームワーク側がジェネリクスを活用した設計になっているので、TypeScriptの型システムとの相性がいい。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?