Day 027 — HTTPサーバー実装(net/http)

2026-08-24 🔵 中級者 / Phase 2 実装 HTTPサーバー実装(net/http)

📚 背景知識(読んでから問題へ)

Day 026では、EmailSenderインターフェース経由のDI(依存性注入)によってOrderServiceをテスト可能な設計にしました。今日はそのOrderServiceを、実際にHTTP経由で呼び出せるAPIとして公開します。

Goには「Webフレームワーク」に相当するものが標準では存在しません。その代わり標準ライブラリのnet/httpパッケージが、驚くほど小さな抽象化だけでHTTPサーバーの全体を成立させています。核となるのはたった1つのインターフェースです。

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

「HTTPリクエストを受け取り、レスポンスを書き込む」という操作さえ満たせば、それはhttp.Handlerです。関数をそのままHandlerとして使えるようにするhttp.HandlerFuncというアダプタ型も用意されています。

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

ルーティングはhttp.ServeMuxが担当します。パスとハンドラを紐づけるだけの単純な仕組みで、Express.js(Node.js)やSpring MVCのような高機能ルーターと比べると機能は最小限ですが、「シンプルさを保ち、必要なら外部ライブラリで拡張する」というGoの設計思想がここにも表れています。

ミドルウェアもフレームワーク固有の概念ではなく、「http.Handlerを受け取り、別のhttp.Handlerを返す関数」というただの高階関数として実現されます。

type Middleware func(http.Handler) http.Handler

func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// next の前後に処理を差し込める
		next.ServeHTTP(w, r)
	})
}

next.ServeHTTPを呼ぶ前後に処理を挟めるため、ロギング・認証・リクエストID付与などを「本来のハンドラのコードを一切変更せずに」横断的に追加できます。これは関数を関数でラップする、というGoの基本文法だけで成立する仕組みであり、Day 026で見た「DIコンテナという魔法を使わない」設計思想の延長線上にあります。

テストの面でも標準ライブラリだけで完結します。net/http/httptestパッケージのhttptest.NewRequest(テスト用リクエストの生成)とhttptest.NewRecorder(レスポンスを記録するhttp.ResponseWriter実装)を使えば、実際にポートをリッスンせずにハンドラを直接呼び出してテストできます。

📝 問題

Day 026で作ったOrderService(EmailSenderをDIで受け取る設計)をベースに、注文確定APIをHTTPサーバーとして実装してください。

Part A: ハンドラの実装

POST /ordersエンドポイントを実装してください。

  1. リクエストボディは以下のJSON形式とする。
    {"id": "A1", "customer_email": "a@example.com", "total_yen": 1000}
  2. OrderHandler構造体を定義し、*OrderServiceをコンストラクタ(NewOrderHandler(svc *OrderService) *OrderHandler)経由で受け取る(Day 026と同じDIパターンをHTTP層でも踏襲する)
  3. ConfirmOrder(w http.ResponseWriter, r *http.Request)メソッドで以下を実装する。
    • POST以外のメソッドは405 Method Not Allowed
    • JSONのパースに失敗した場合は400 Bad Request
    • OrderService.ConfirmOrderが「注文合計が不正」というエラーを返した場合は400 Bad Request
    • OrderService.ConfirmOrderが「メール送信に失敗した」というエラーを返した場合は500 Internal Server Error
    • 成功時は201 Createdで、確定した注文をJSONで返す
  4. OrderService側は、上記2種類のエラーをハンドラ側がerrors.Is等で判別できるように、センチネルエラー(例: ErrInvalidOrder)を使ってラップして返すよう変更する(Part Aのハンドラが「合計が0円以下かどうか」を再度自分で判定するような重複ロジックを書かないこと)

Part B: ロギングミドルウェアの実装

以下を満たすloggingMiddleware(next http.Handler) http.Handlerを実装してください。

  1. リクエストメソッド・パス・レスポンスのステータスコード・処理時間をログ出力する
  2. ステータスコードを記録するため、http.ResponseWriterをラップした型(WriteHeaderをオーバーライドして記録する)を用意する
  3. http.ServeMuxにハンドラを登録した後、loggingMiddlewareでラップしてmain関数からhttp.ListenAndServeに渡す構成にする

Part C: httptestによるテスト

httptestパッケージを使い、以下をテーブル駆動テストで検証してください(ネットワークを実際にリッスンしない)。

  1. 正常な注文 → 201が返る
  2. 合計0円以下の注文 → 400が返る
  3. 不正なJSON → 400が返る
  4. メール送信が失敗するケース(テスト用のSendが常にエラーを返すスタブ実装を使う)→ 500が返る
  5. GETメソッドでのリクエスト → 405が返る
  6. (余力があれば)loggingMiddlewareが、内側のハンドラが書き込んだステータスコードを正しく記録できていることを検証するテスト

go run . / go test -v ./...でそのまま動作する完全なコードとして提示すること。

🔍 ヒント(段階的開示)

ヒント1 — 方向性

「HTTPの関心事(メソッドの判定・JSONのパース・ステータスコードの決定)」と「業務ロジックの関心事(注文が有効か・メールが送れたか)」を分離するのが出発点です。Day 026のOrderServiceは一切HTTPを知らないまま維持し、OrderHandlerという薄い層だけを新しく追加してHTTPとの橋渡しをします。

ヒント2 — アプローチ
  • エラー種別の判別には、errors.Newで作った変数(センチネルエラー)を%wでラップする方法が使えます。
    var ErrInvalidOrder = errors.New("invalid order total")
    
    func (s *OrderService) ConfirmOrder(order Order) error {
    	if order.TotalYen <= 0 {
    		return fmt.Errorf("%w: %d", ErrInvalidOrder, order.TotalYen)
    	}
    	// ...
    }
    ハンドラ側ではerrors.Is(err, ErrInvalidOrder)で判定すれば、「合計金額のチェックロジック」をOrderHandlerが再実装せずに済みます。
  • ステータスコードを記録するラップ型は次の形が定石です。
    type statusRecorder struct {
    	http.ResponseWriter
    	status int
    }
    func (r *statusRecorder) WriteHeader(status int) {
    	r.status = status
    	r.ResponseWriter.WriteHeader(status)
    }
    埋め込み(embedding)によりstatusRecorderは自動的にhttp.ResponseWriterを満たしつつ、WriteHeaderだけを差し替えられます(Day 021のembeddingの実務応用です)。
  • httptest.NewRequest(method, target, body io.Reader)とhttptest.NewRecorder()を組み合わせ、handler.ServeHTTP(rec, req)(またはメソッドを直接呼ぶ)でテストします。
ヒント3 — コード骨格
var ErrInvalidOrder = errors.New("invalid order total")

type OrderHandler struct {
	svc *OrderService
}

func NewOrderHandler(svc *OrderService) *OrderHandler {
	return &OrderHandler{svc: svc}
}

func (h *OrderHandler) ConfirmOrder(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		writeJSON(w, http.StatusMethodNotAllowed, errorResponse{Error: "method not allowed"})
		return
	}
	var order Order
	if err := json.NewDecoder(r.Body).Decode(&order); err != nil {
		writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()})
		return
	}
	if err := h.svc.ConfirmOrder(order); err != nil {
		if errors.Is(err, ErrInvalidOrder) {
			writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()})
			return
		}
		writeJSON(w, http.StatusInternalServerError, errorResponse{Error: err.Error()})
		return
	}
	writeJSON(w, http.StatusCreated, order)
}

// テストではこう組み立てる(ネットワークをリッスンしない)
req := httptest.NewRequest(http.MethodPost, "/orders", strings.NewReader(`{"id":"A1",...}`))
rec := httptest.NewRecorder()
handler.ConfirmOrder(rec, req)
// rec.Code / rec.Body を検証する

✅ 模範解答

ファイル: main.go

package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"time"
)

// --- Day 026から引き継ぐドメイン層 ---

// Order は注文情報を表す。JSONタグを付け、HTTPボディとの相互変換に使う。
type Order struct {
	ID            string `json:"id"`
	CustomerEmail string `json:"customer_email"`
	TotalYen      int    `json:"total_yen"`
}

// EmailSender は「メールを送る」という意図だけを表現するインターフェース。
type EmailSender interface {
	Send(to, subject, body string) error
}

// SMTPEmailSender は EmailSender の本番実装。
type SMTPEmailSender struct {
	Host string
	Port int
	From string
}

func (s *SMTPEmailSender) Send(to, subject, body string) error {
	addr := fmt.Sprintf("%s:%d", s.Host, s.Port)
	_ = addr
	fmt.Printf("[SMTP %s] to=%s subject=%s\n", addr, to, subject)
	return nil
}

// ErrInvalidOrder は「注文内容が不正」というエラーを表すセンチネルエラー。
// ハンドラ側はこの変数を errors.Is で判定し、400/500 のステータスコードを振り分ける。
var ErrInvalidOrder = errors.New("invalid order total")

// OrderService は EmailSender インターフェース型のフィールドだけを持つ。HTTPを一切知らない。
type OrderService struct {
	sender EmailSender
}

func NewOrderService(sender EmailSender) *OrderService {
	return &OrderService{sender: sender}
}

func (s *OrderService) ConfirmOrder(order Order) error {
	if order.TotalYen <= 0 {
		return fmt.Errorf("%w: %d", ErrInvalidOrder, order.TotalYen)
	}
	body := fmt.Sprintf("ご注文 #%s(合計%d円)を承りました。", order.ID, order.TotalYen)
	if err := s.sender.Send(order.CustomerEmail, "ご注文確認", body); err != nil {
		return fmt.Errorf("send confirmation email: %w", err)
	}
	return nil
}

// --- HTTP層(今日の新規追加分) ---

// errorResponse はエラー時のJSONレスポンス形式。
type errorResponse struct {
	Error string `json:"error"`
}

// writeJSON はステータスコードと値をJSONとして書き込む共通ヘルパー。
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

// OrderHandler は OrderService を HTTP 経由で呼び出す薄い橋渡し層。
// Day 026 と同じく、依存(*OrderService)はコンストラクタ経由で受け取る。
type OrderHandler struct {
	svc *OrderService
}

func NewOrderHandler(svc *OrderService) *OrderHandler {
	return &OrderHandler{svc: svc}
}

// ConfirmOrder は POST /orders のハンドラ。
func (h *OrderHandler) ConfirmOrder(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		w.Header().Set("Allow", http.MethodPost)
		writeJSON(w, http.StatusMethodNotAllowed, errorResponse{Error: "method not allowed"})
		return
	}

	var order Order
	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()
	if err := dec.Decode(&order); err != nil {
		writeJSON(w, http.StatusBadRequest, errorResponse{Error: "invalid request body: " + err.Error()})
		return
	}

	if err := h.svc.ConfirmOrder(order); err != nil {
		// 「合計金額が不正か」を再判定せず、OrderServiceが返したエラーの種類だけで振り分ける。
		if errors.Is(err, ErrInvalidOrder) {
			writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()})
			return
		}
		writeJSON(w, http.StatusInternalServerError, errorResponse{Error: err.Error()})
		return
	}

	writeJSON(w, http.StatusCreated, order)
}

// --- ミドルウェア ---

// statusRecorder は http.ResponseWriter を埋め込み、WriteHeaderで渡されたステータスコードを記録する。
type statusRecorder struct {
	http.ResponseWriter
	status int
}

func (r *statusRecorder) WriteHeader(status int) {
	r.status = status
	r.ResponseWriter.WriteHeader(status)
}

// loggingMiddleware は http.Handler を受け取り、ログ出力付きの http.Handler を返す。
// next のコード自体には一切手を入れず、前後に処理を差し込む「関数によるラップ」だけで実現する。
func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(rec, r)
		log.Printf("%s %s -> %d (%s)", r.Method, r.URL.Path, rec.status, time.Since(start))
	})
}

func newMux(svc *OrderService) http.Handler {
	h := NewOrderHandler(svc)
	mux := http.NewServeMux()
	mux.HandleFunc("/orders", h.ConfirmOrder)
	return loggingMiddleware(mux)
}

func main() {
	sender := &SMTPEmailSender{Host: "smtp.example.com", Port: 587, From: "shop@example.com"}
	svc := NewOrderService(sender)
	handler := newMux(svc)

	log.Println("listening on :8080")
	if err := http.ListenAndServe(":8080", handler); err != nil {
		log.Fatal(err)
	}
}

ファイル: main_test.go

package main

import (
	"bytes"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
)

// stubSender はテスト用の EmailSender 実装。送信成功時にカウントし、
// err が設定されている場合は常にそのエラーを返す。
type stubSender struct {
	sentCount int
	err       error
}

func (s *stubSender) Send(to, subject, body string) error {
	if s.err != nil {
		return s.err
	}
	s.sentCount++
	return nil
}

func TestOrderHandler_ConfirmOrder(t *testing.T) {
	tests := []struct {
		name       string
		method     string
		body       string
		senderErr  error
		wantStatus int
	}{
		{
			name:       "正常な注文は201を返す",
			method:     http.MethodPost,
			body:       `{"id":"A1","customer_email":"a@example.com","total_yen":1000}`,
			wantStatus: http.StatusCreated,
		},
		{
			name:       "合計0円以下は400を返す",
			method:     http.MethodPost,
			body:       `{"id":"A2","customer_email":"b@example.com","total_yen":0}`,
			wantStatus: http.StatusBadRequest,
		},
		{
			name:       "不正なJSONは400を返す",
			method:     http.MethodPost,
			body:       `{invalid`,
			wantStatus: http.StatusBadRequest,
		},
		{
			name:       "送信エラー時は500を返す",
			method:     http.MethodPost,
			body:       `{"id":"A3","customer_email":"c@example.com","total_yen":500}`,
			senderErr:  errors.New("smtp connection refused"),
			wantStatus: http.StatusInternalServerError,
		},
		{
			name:       "GETは405を返す",
			method:     http.MethodGet,
			body:       "",
			wantStatus: http.StatusMethodNotAllowed,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sender := &stubSender{err: tt.senderErr}
			svc := NewOrderService(sender)
			handler := NewOrderHandler(svc)

			req := httptest.NewRequest(tt.method, "/orders", bytes.NewBufferString(tt.body))
			rec := httptest.NewRecorder()

			handler.ConfirmOrder(rec, req)

			if rec.Code != tt.wantStatus {
				t.Fatalf("status = %d, want %d (body=%s)", rec.Code, tt.wantStatus, rec.Body.String())
			}
		})
	}
}

// TestLoggingMiddleware_RecordsStatus は、内側のハンドラが書き込んだステータスコードを
// statusRecorder が正しく記録し、外側までそのまま伝搬することを検証する。
func TestLoggingMiddleware_RecordsStatus(t *testing.T) {
	next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusTeapot)
	})
	wrapped := loggingMiddleware(next)

	req := httptest.NewRequest(http.MethodGet, "/ping", nil)
	rec := httptest.NewRecorder()

	wrapped.ServeHTTP(rec, req)

	if rec.Code != http.StatusTeapot {
		t.Fatalf("status = %d, want %d", rec.Code, http.StatusTeapot)
	}
}
▶ 実行結果を見る(go run . / go test -v ./... で検証済み)
$ go run .
2026/08/24 10:00:00 listening on :8080
(別ターミナルから)
$ curl -s -X POST localhost:8080/orders -d '{"id":"A1","customer_email":"a@example.com","total_yen":1000}'
{"id":"A1","customer_email":"a@example.com","total_yen":1000}
サーバー側ログ: 2026/08/24 10:00:05 POST /orders -> 201 (312µs)

$ go test -v ./...
=== RUN   TestOrderHandler_ConfirmOrder
=== RUN   TestOrderHandler_ConfirmOrder/正常な注文は201を返す
=== RUN   TestOrderHandler_ConfirmOrder/合計0円以下は400を返す
=== RUN   TestOrderHandler_ConfirmOrder/不正なJSONは400を返す
=== RUN   TestOrderHandler_ConfirmOrder/送信エラー時は500を返す
=== RUN   TestOrderHandler_ConfirmOrder/GETは405を返す
--- PASS: TestOrderHandler_ConfirmOrder (0.00s)
    --- PASS: TestOrderHandler_ConfirmOrder/正常な注文は201を返す (0.00s)
    --- PASS: TestOrderHandler_ConfirmOrder/合計0円以下は400を返す (0.00s)
    --- PASS: TestOrderHandler_ConfirmOrder/不正なJSONは400を返す (0.00s)
    --- PASS: TestOrderHandler_ConfirmOrder/送信エラー時は500を返す (0.00s)
    --- PASS: TestOrderHandler_ConfirmOrder/GETは405を返す (0.00s)
=== RUN   TestLoggingMiddleware_RecordsStatus
--- PASS: TestLoggingMiddleware_RecordsStatus (0.00s)
PASS
ok  	example.com/day027	0.003s

go.mod・main.go・main_test.goを1モジュール(module example.com/day027)として配置し、go run . / go test -v ./...で検証済みです。テストは実際にポートをリッスンせず、httptestだけでミリ秒単位で完走します。

🪜 Step-by-Step 解説

1
ドメイン層をHTTPから独立させたまま維持する
type OrderService struct {
	sender EmailSender
}
Day 026のOrderServiceにはHTTPの知識を一切持ち込みません。net/httpのimportすらOrderServiceが定義されているファイル内には登場させない、という制約を守ることで、「注文確定ロジック」と「HTTPでの公開方法」が独立に変更できる状態を保ちます。
2
エラー種別をセンチネルエラーで表現する
var ErrInvalidOrder = errors.New("invalid order total")

func (s *OrderService) ConfirmOrder(order Order) error {
	if order.TotalYen <= 0 {
		return fmt.Errorf("%w: %d", ErrInvalidOrder, order.TotalYen)
	}
	// ...
}
Day 018で学んだerrors.Is/%wによるエラーラッピングをここで実務的に使います。「入力が悪いのか(400)」「外部サービスが落ちているのか(500)」という区別を、ハンドラ側がドメインロジックを再実装せずに判定できるようにする設計です。
3
HTTPの関心事だけを扱う薄いハンドラを書く
func (h *OrderHandler) ConfirmOrder(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost { /* 405 */ }
	// JSONデコード → 400
	if err := h.svc.ConfirmOrder(order); err != nil {
		if errors.Is(err, ErrInvalidOrder) { /* 400 */ }
		/* 500 */
	}
	/* 201 */
}
OrderHandlerが持つ知識は「HTTPメソッドの判定」「JSONのエンコード/デコード」「エラー種別とステータスコードの対応表」だけです。「注文合計が0円以下かどうか」という業務ルールそのものは一切知りません。
4
ミドルウェアで横断的関心事を分離する
func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(rec, r)
		log.Printf(...)
	})
}
ロギングという「全エンドポイントに共通する関心事」を、OrderHandlerのコードに一切触れずに追加します。http.Handlerを受け取ってhttp.Handlerを返す、というただの関数合成がミドルウェアの正体です。
5
httptestでネットワークなしにハンドラをテストする
req := httptest.NewRequest(tt.method, "/orders", bytes.NewBufferString(tt.body))
rec := httptest.NewRecorder()
handler.ConfirmOrder(rec, req)
http.ListenAndServeを実際に呼ばず、http.Handlerインターフェースを満たすメソッドを直接呼び出すだけでテストが完結します。これにより、テストはポート番号の衝突やタイムアウトを気にせず、ミリ秒単位で完走します。

💡 設計思想・なぜこう書くのか

📌
フレームワークではなく、1つのインターフェースで全体を成立させる: http.HandlerはServeHTTP(ResponseWriter, *Request)という1メソッドしか持ちません。ルーティング(ServeMux)もミドルウェアも「この1つのインターフェースを満たすものを組み合わせる」だけで実現されており、Spring MVCのようなアノテーション駆動の巨大なフレームワークが介在しません。Day 020で学んだ「小さいインターフェース」の原則が、標準ライブラリの設計そのものに体現されている好例です。
📌
ミドルウェアは「機能」ではなく「関数合成のパターン」: func(http.Handler) http.Handlerという型は、Goの言語機能として特別扱いされているわけではありません。「関数を引数に取り、関数を返す」という、Goの基本文法だけで表現できる高階関数です。フレームワークが提供する専用の「ミドルウェアAPI」を学ぶ必要がなく、Goの文法を理解していれば誰でも同じパターンで拡張できます。

🌐 他言語との比較

観点Go(net/http)Node.js(Express.js)Python(Flask)Java(Spring Boot)
ハンドラの実体http.Handlerインターフェース(ServeHTTP1メソッド)またはhttp.HandlerFunc(req, res, next) => {}という関数@app.routeデコレータを付けた関数@RestController/@RequestMappingアノテーション付きメソッド
ルーティングhttp.ServeMux(標準ライブラリ、最小限の機能)Expressのapp.get()等(フレームワーク組み込み)Flaskの@app.route(フレームワーク組み込み)アノテーションベースでSpringが実行時に解決
ミドルウェアの実現方法func(http.Handler) http.Handlerという関数合成app.use(fn)で登録する専用のnextコールバック方式before_request/after_requestフックまたはデコレータFilter/Interceptorという専用のフレームワークAPI
標準ライブラリだけで完結するかする(外部依存ゼロでAPIサーバーが書ける)しない(Node.js標準のhttpだけでは大幅に低機能)しない(Flask自体が外部パッケージ)しない(Spring Bootは大規模な外部フレームワーク)
テストの書き方httptestでhttp.Handlerを直接呼ぶ(標準ライブラリのみ)supertest等の外部ライブラリでHTTPリクエストをシミュレートFlaskのtest_client()(フレームワーク組み込み)MockMvc等のSpring Test機能(フレームワーク組み込み)

GoのAPIサーバーが「外部フレームワークなしで実務水準のものが書ける」のは、net/httpが持つ抽象化の少なさによるものです。一方でExpress.jsやSpring Bootは、ルーティングの記法やミドルウェアの実行順序といった「フレームワーク固有の作法」を別途学ぶ必要があり、その分だけ高機能・高生産性になっている側面もあります。

🏆 実務での使いどころ

  • REST APIの実装: 今回のような「JSONを受け取り、ドメインロジックを呼び、JSONを返す」パターンは、実務のAPIサーバー実装の基本形そのものです
  • 横断的関心事の分離: ロギングに加えて、認証(Authorizationヘッダーの検証)、リクエストID付与、パニックからのリカバリ(recover)、CORSヘッダーの付与なども同じミドルウェアパターンで実装できます
  • エラーとHTTPステータスコードのマッピング設計: 「ドメイン層のエラー(センチネルエラー・カスタムエラー型)」と「HTTPステータスコード」の対応表を明示的に設計しておくことで、新しいエンドポイントを追加するたびに同じ判断基準を再利用できます
  • ヘルスチェック・グレースフルシャットダウンの土台: 今回作ったnewMuxのような「http.Handlerを組み立てる関数」は、Phase 3で扱うgraceful shutdown実装(シグナル受信時にhttp.Server.Shutdownを呼ぶ)の土台にそのまま使えます

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
ハンドラの中でドメイン層のバリデーションルール(TotalYen <= 0等)をHTTP層でも再実装してしまう「念のため」二重にチェックしておきたい気持ちが働くドメイン層が返すエラーの種類(センチネルエラー等)で判定すれば、業務ルールの実体は1箇所(OrderService)だけに存在させられる。二重実装はルール変更時の修正漏れの温床になる
http.ResponseWriterに対してWriteHeaderを呼んだ後にヘッダーを追加で設定しようとするヘッダー設定とステータスコード決定の順序を意識していないWriteHeaderを呼んだ時点でHTTPレスポンスのヘッダー部分は確定して送信される。Header().Set(...)は必ずWriteHeaderより前に呼ぶ必要がある
ミドルウェアをmux.Handleの登録順やhttp.ListenAndServeに渡す順序を気にせず適当に重ねてしまう「関数を呼ぶだけだから順序は関係ない」と思い込むloggingMiddleware(mux)のように外側から包んだ順が実際の実行順(外側のミドルウェアが先に実行され、内側のハンドラに近いほど後に実行される)になる。認証ミドルウェアをロギングミドルウェアより内側に置くと、認証失敗のリクエストがログに残らない、といった問題が起きうる
テストのためにhttptest.NewServerで毎回実際にポートをリッスンしてテストしてしまう「HTTPサーバーのテストだから実際に立ち上げるべき」と考えてしまうhttp.Handlerはただのインターフェースなので、ServeHTTP(またはハンドラのメソッド)を直接呼ぶだけでテストできる。実際にポートをリッスンするhttptest.NewServerが必要になるのは、http.Clientを含めた統合的な通信経路までテストしたい場合に限られる

🚀 次のステップ

  • 発展: 今回はPOST /ordersの1エンドポイントのみでしたが、GET /orders/{id}(注文照会)を追加し、OrderRepositoryインターフェース(Day 026の発展課題で触れた設計)も注入してみましょう。複数の依存を持つハンドラの構成に慣れる練習になります
  • 次回予告: Day 028 — HTTPクライアント実装(実装)。今日はサーバー側を実装しましたが、次回はhttp.Clientを使ったクライアント実装(タイムアウト設定・リトライ・レスポンスのデコード)を学びます

🎯 自己評価

自分の回答

気づき・メモ