mcpField
G
go-chi-handler

go-chi-handler

github.com/cristiano-pacheco開発147 回インストール1 日前に更新
リポジトリ

go-chi-handler について

go-chi-handler は、Go バックエンド向けの Chi HTTP ハンドラーを生成するコーディングスキルです。DTO マッピング、エラー処理、Swagger アノテーション、Fx 接続の規約を定めています。

主な機能

  1. 01REST HTTP ハンドラーの生成
  2. 02リクエスト・レスポンス DTO のマッピング
  3. 03Swagger アノテーションの追加
  4. 04一貫したエラー処理
  5. 05Fx 接続手順の提示

活用シーン

  1. 01REST 操作用のハンドラー作成
  2. 02一覧・作成・更新・削除エンドポイントの実装
  3. 03ID 指定エンドポイントのパスパラメーター解析

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/cristiano-pacheco/ai-tools --skill go-chi-handler

インストール後、Claude Code で/go-chi-handlerを入力して読み込まれたか確認します。

詳細

Go Chi ハンドラー

Go バックエンド用の Chi HTTP ハンドラー実装を生成します。

使用する場面

  • REST 操作用の HTTP エンドポイントハンドラーを作成する
  • 一覧、作成、更新、削除、取得のハンドラー
  • ユースケースのオーケストレーションを伴うリクエスト/レスポンス DTO のマッピング
  • Swagger アノテーション付きエンドポイント

ハンドラーの構造

場所:internal/modules/<module>/http/chi/handler/<resource>_handler.go

package handler

import (
	"net/http"
	"fmt"
	"strconv"
	"strings"

	brickserrs "github.com/cristiano-pacheco/bricks/pkg/errs"
	"github.com/cristiano-pacheco/bricks/pkg/http/request"
	"github.com/cristiano-pacheco/bricks/pkg/http/response"
	"github.com/cristiano-pacheco/bricks/pkg/logger"
	"github.com/cristiano-pacheco/bricks/pkg/ucdecorator"
	"github.com/cristiano-pacheco/pingo/internal/modules/<module>/http/dto"
	"github.com/cristiano-pacheco/pingo/internal/modules/<module>/usecase"
	"github.com/go-chi/chi/v5"
)

type ResourceHandler struct {
	resourceCreateUseCase ucdecorator.UseCase[usecase.ResourceCreateInput, usecase.ResourceCreateOutput]
	resourceListUseCase   ucdecorator.UseCase[usecase.ResourceListInput, usecase.ResourceListOutput]
	resourceUpdateUseCase ucdecorator.UseCase[usecase.ResourceUpdateInput, usecase.ResourceUpdateOutput]
	resourceDeleteUseCase ucdecorator.UseCase[usecase.ResourceDeleteInput, usecase.ResourceDeleteOutput]
	resourceGetUseCase    ucdecorator.UseCase[usecase.ResourceGetInput, usecase.ResourceGetOutput]
	errorHandler          response.ErrorHandler
	logger                logger.Logger
}

func NewResourceHandler(
	resourceCreateUseCase ucdecorator.UseCase[usecase.ResourceCreateInput, usecase.ResourceCreateOutput],
	resourceListUseCase ucdecorator.UseCase[usecase.ResourceListInput, usecase.ResourceListOutput],
	resourceUpdateUseCase ucdecorator.UseCase[usecase.ResourceUpdateInput, usecase.ResourceUpdateOutput],
	resourceDeleteUseCase ucdecorator.UseCase[usecase.ResourceDeleteInput, usecase.ResourceDeleteOutput],
	resourceGetUseCase ucdecorator.UseCase[usecase.ResourceGetInput, usecase.ResourceGetOutput],
	errorHandler response.ErrorHandler,
	logger logger.Logger,
) *ResourceHandler {
	return &ResourceHandler{
		resourceCreateUseCase: resourceCreateUseCase,
		resourceListUseCase:   resourceListUseCase,
		resourceUpdateUseCase: resourceUpdateUseCase,
		resourceDeleteUseCase: resourceDeleteUseCase,
		resourceGetUseCase:    resourceGetUseCase,
		errorHandler:          errorHandler,
		logger:                logger,
	}
}

要点:

  • ユースケースには常にジェネリックインターフェース ucdecorator.UseCase[Input, Output] を使用します。具象型の *usecase.ResourceUseCase ポインターは使用しません
  • brickserrs はエイリアスを付けてインポートします:brickserrs "github.com/cristiano-pacheco/bricks/pkg/errs"
  • コンストラクターはポインター *ResourceHandler を返します

DTO(データ転送オブジェクト)

リクエストとレスポンスの DTO は、internal/modules/<module>/http/dto/<resource>_dto.go で定義します。

一般的な DTO の構造:

package dto

type CreateResourceRequest struct {
	Field1 string `json:"field1"`
	Field2 int    `json:"field2"`
}

type CreateResourceResponse struct {
	ID     uint64 `json:"id"`
	Field1 string `json:"field1"`
	Field2 int    `json:"field2"`
}

type UpdateResourceRequest struct {
	Field1 string `json:"field1"`
	Field2 int    `json:"field2"`
}

type ResourceResponse struct {
	ID     uint64 `json:"id"`
	Field1 string `json:"field1"`
	Field2 int    `json:"field2"`
}

要点:

  • DTO はユースケースの入力やモデルとは分けて、HTTP トランスポート層に配置します
  • シリアライズには JSON タグを使用します
  • DTO は HTTP コントラクトに特化させ、ドメインロジックを含めないようにします

ハンドラーメソッドのパターン

すべてのハンドラーメソッドには Handle プレフィックスを付けます:HandleListResources、HandleCreateResource など。

一覧(GET /resources)
// @Summary		List resources
// @Description	Retrieves all resources
// @Tags		Resources
// @Accept		json
// @Produce		json
// @Success		200	{object}	response.Envelope[[]dto.ResourceResponse]	"Successfully retrieved resources"
// @Failure		500	{object}	brickserrs.Error	"Internal server error"
// @Router		/api/v1/resources [get]
func (h *ResourceHandler) HandleListResources(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	output, err := h.resourceListUseCase.Execute(ctx, usecase.ResourceListInput{})
	if err != nil {
		h.logger.Error("failed to list resources", logger.Error(err))
		h.errorHandler.Error(w, err)
		return
	}

	resources := make([]dto.ResourceResponse, 0, len(output.Resources))
	for _, resource := range output.Resources {
		resources = append(resources, dto.ResourceResponse{
			ID:   resource.ID,
			Name: resource.Name,
			// ... map other fields
		})
	}

	if err = response.JSON(w, http.StatusOK, resources, http.Header{}); err != nil {
		h.logger.Error("failed to write list resources response", logger.Error(err))
		h.errorHandler.Error(w, err)
		return
	}
}
作成(POST /resources)
// @Summary		Create resource
// @Description	Creates a new resource
// @Tags		Resources
// @Accept		json
// @Produce		json
// @Param		request	body	dto.CreateResourceRequest	true	"Resource data"
// @Success		201	{object}	response.Envelope[dto.CreateResourceResponse]	"Successfully created resource"
// @Failure		422	{object}	brickserrs.Error	"Invalid request format or validation error"
// @Failure		500	{object}	brickserrs.Error	"Internal server error"
// @Router		/api/v1/resources [post]
func (h *ResourceHandler) HandleCreateResource(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	var createRequest dto.CreateResourceRequest
	if err := request.ReadJSON(w, r, &createRequest); err != nil {
		h.logger.Error("failed to parse request body", logger.Error(err))
		h.errorHandler.Error(w, err)
		return
	}

	output, err := h.resourceCreateUseCase.Execute(ctx, usecase.ResourceCreateInput{
		Name: createRequest.Name,
		// ... map other fields
	})
	if err != nil {
		h.logger.Error("failed to create resource", logger.Error(err))
		h.errorHandler.Error(w, err)
		return
	}

	createResponse := dto.CreateResourceResponse{
		ID:   output.ID,
		Name: output.Name,
		// ... map other fields
	}

	if err = response.JSON(w, http.StatusCreated, createResponse, http.Header{}); err != nil {
		h.logger.Error("failed to write create resource response", logger.Error(err))
		h.errorHandler.Error(w, err)
		return
	}
}
更新(PUT /resources/:id)
// @Summary		Update resource
// @Description	Updates an existing resource
// @Tags		Resources
// @Accept		json
// @Produce		json
// @Param		id		path	int						true	"Resource ID"
// @Param		request	body	dto.UpdateResourceRequest	true	"Resource data"
// @Success		204		"Successfully updated resource"
// @Failure		422	{object}	brickserrs.Error	"Invalid request format or validation error"
// @Failure		404	{object}	brickserrs.Error	"Resource not found"
// @Failure		500	{object}	brickserrs.Error	"Internal server error"
// @Router		/api/v1/resources/{id} [put]
func (h *ResourceHandler) HandleUpdateResource(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	id, err := h.parseUintPathParam(r, "id")
	if err != nil {
		h.logger.Error("invalid resource id", logger.Error(err))
		h.errorHandler.Error(w, err)
		return
	}

	var updateRequest dto.UpdateResourceRequest
	if err = request.ReadJSON(w, r, &updateRequest); err != nil {
		h.logger.Error("failed to parse request body", logger.Error(err))
		h.errorHandler.Error(w, err)
		return
	}

	if _, err = h.resourceUpdateUseCase.Execute(ctx, usecase.ResourceUpdateInput{
		ID:   id,
		Name: updateRequest.Name,
		// ... map other fields
	}); err != nil {
		h.logger.Error("failed to update resource", logger.Error(err))
		h.errorHandler.Error(w, err)
		return
	}

	response.NoContent(w)
}
削除(DELETE /resources/:id)
// @Summary		Delete resource
// @Description	Deletes an existing resource
// @Tags		Resources
// @Accept		json
// @Produce		json
// @Param		id	path	int	true	"Resource ID"
// @Success		204		"Successfully deleted resource"
// @Failure		404	{object}	brickserrs.Error	"Resource not found"
// @Failure		500	{object}	brickserrs.Error	"Internal server error"
// @Router		/api/v1/resources/{id} [delete]
func (h *ResourceHandler) HandleDeleteResource(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	id, err := h.parseUintPathParam(r, "id")
	if err != nil {
		h.logger.Error("invalid resource id", logger.Error(err))
		h.errorHandler.Error(w, err)
		return
	}

	if _, err = h.resourceDeleteUseCase.Execute(ctx, usecase.ResourceDeleteInput{ID: id}); err != nil {
		h.logger.Error("failed to delete resource", logger.Error(err))
		h.errorHandler.Error(w, err)
		return
	}

	response.NoContent(w)
}
ID による取得(GET /resources/:id)
// @Summary		Get resource
// @Description	Retrieves a resource by ID
// @Tags		Resources
// @Accept		json
// @Produce		json
// @Param		id	path	int	true	"Resource ID"
// @Success		200	{object}	response.Envelope[dto.ResourceResponse]	"Successfully retrieved resource"
// @Failure		404	{object}	brickserrs.Error	"Resource not found"
// @Failure		500	{object}	brickserrs.Error	"Internal server error"
// @Router		/api/v1/resources/{id} [get]
func (h *ResourceHandler) HandleGetResource(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	id, err := h.parseUintPathParam(r, "id")
	if err != nil {
		h.logger.Error("invalid resource id", logger.Error(err))
		h.errorHandler.Error(w, err)
		return
	}

	output, err := h.resourceGetUseCase.Execute(ctx, usecase.ResourceGetInput{ID: id})
	if err != nil {
		h.logger.Error("failed to get resource", logger.Error(err))
		h.errorHandler.Error(w, err)
		return
	}

	resourceResponse := dto.ResourceResponse{
		ID:   output.ID,
		Name: output.Name,
		// ... map other fields
	}

	if err = response.JSON(w, http.StatusOK, resourceResponse, http.Header{}); err != nil {
		h.logger.Error("failed to write get resource response", logger.Error(err))
		h.errorHandler.Error(w, err)
		return
	}
}
URL パラメーターのヘルパー(プライベートメソッド)

パスパラメーターを解析するハンドラーでは、単独の関数ではなく、構造体のプライベートメソッドを使用します。

func (h *ResourceHandler) parseUintPathParam(r *http.Request, paramName string) (uint64, error) {
	value := strings.TrimSpace(chi.URLParam(r, paramName))
	if value == "" {
		return 0, h.newBadRequestError(fmt.Sprintf("missing path param %q", paramName))
	}

	parsed, err := strconv.ParseUint(value, 10, 64)
	if err != nil {
		return 0, h.newBadRequestError(fmt.Sprintf("invalid path param %q", paramName))
	}

	return parsed, nil
}

func (h *ResourceHandler) newBadRequestError(message string) *brickserrs.Error {
	return brickserrs.New("MODULE_90", message, http.StatusBadRequest, nil)
}

リクエスト/レスポンスのマッピング

ハンドラーメソッドは、HTTP リクエスト/レスポンス(internal/modules/<module>/http/dto の DTO)とユースケースの入出力を橋渡しします。

リクエストからユースケース入力への変換
// Decode request
var req dto.CreateResourceRequest

err := request.ReadJSON(w, r, &req)
if err != nil {
	h.logger.Error("failed to parse request body", logger.Error(err))
	h.errorHandler.Error(w, err)
	return
}

// Map to use case input
input := usecase.ResourceCreateInput{
	Field1: req.Field1,
	Field2: req.Field2,
}
ユースケース出力からレスポンスへの変換
// Execute use case
output, err := h.resourceCreateUseCase.Execute(ctx, input)
if err != nil {
	h.logger.Error("failed to create resource", logger.Error(err))
	h.errorHandler.Error(w, err)
	return
}

// Map to response DTO
response := dto.CreateResourceResponse{
	ID:     output.ID,
	Field1: output.Field1,
	Field2: output.Field2,
}

Swagger アノテーションのルール

  1. @Summary:操作の概要(例:「リソース一覧の取得」、「リソースの作成」)
  2. @Description:エンドポイントの動作の詳細な説明
  3. @Tags:リソース名の複数形(例:「リソース」、「連絡先」)
  4. @Accept:常に json
  5. @Produce:常に json
  6. @Security:認証が必要な場合は BearerAuth を追加します
  7. @Param:パスパラメーターとリクエストボディを定義します
    • パスパラメーター:@Param id path int true "Resource ID"
    • リクエストボディ:@Param request body dto.CreateResourceRequest true "Resource data"
  8. @Success:ステータスコードとレスポンス型
    • 200:{object} response.Envelope[dto.ResourceResponse]
    • 201:{object} response.Envelope[dto.CreateResourceResponse]
    • 204:コンテンツなし。説明文字列のみ
  9. @Failure:一般的なエラー(404、422、500)には {object} brickserrs.Error を指定します(エイリアスに注意)
  10. @Router:/api/v1/resources/{id} [method]

エラーハンドリングのパターン

すべてのハンドラーメソッド内のすべてのエラーは、入力検証や URL パラメーターの解析エラーも例外なく、必ず次のパターンに従います。

if err != nil {
	h.logger.Error("descriptive error message", logger.Error(err))
	h.errorHandler.Error(w, err)
	return
}

標準エラーメッセージ:

  • "failed to parse request body" — JSON デコードエラー
  • "invalid resource id" — URL パラメーターの解析エラー
  • "failed to list resources" — 一覧取得ユースケースのエラー
  • "failed to create resource" — 作成ユースケースのエラー
  • "failed to update resource" — 更新ユースケースのエラー
  • "failed to delete resource" — 削除ユースケースのエラー
  • "failed to get resource" — 取得ユースケースのエラー
  • "failed to write list resources response" — JSON レスポンスの書き込みエラー(操作名を含めます)

Fx の配線

internal/modules/<module>/fx.go に追加します:

fx.Provide(handler.NewResourceHandler),

通常、ハンドラーはポートとして公開せず、ルーターに提供します。

重要なルール

  1. 単独関数は禁止:ファイルにメソッドを持つ構造体がある場合、パッケージレベルの単独関数は決して追加しないでください。すべてのヘルパーは構造体の非公開メソッドにしてください(例:parseUintPathParam、newBadRequestError)。
  2. 構造体:すべてのユースケース、response.ErrorHandler、logger.Loggerを含めてください
  3. コンストラクター:ポインター*ResourceHandlerを返す必要があります
  4. コンテキスト:リクエストから必ず取得してください:ctx := r.Context()
  5. リクエストのデコード:request.ReadJSON(w, r, &dto)を使用してください。先に変数を宣言してから、ReadJSONを呼び出します
  6. URL パラメーター:chi.URLParamとstrconv.ParseUintを使用する非公開のparseUintPathParamメソッドで解析してください
  7. エラー処理:すべてのエラーについて、検証エラーや入力の解析エラーも含め、必ずh.logger.Error(...)とh.errorHandler.Error(w, err)を呼び出してからreturnしてください
  8. レスポンスのマッピング:ユースケースの出力をDTOにマッピングしてください。ユースケースの出力を直接返してはいけません
  9. 成功レスポンス:
    • List/Get:response.JSON(w, http.StatusOK, data, http.Header{})
    • Create:response.JSON(w, http.StatusCreated, data, http.Header{})
    • Update/Delete:response.NoContent(w)
  10. Swagger:すべてのハンドラーメソッドに完全なSwaggerアノテーションを追加してください。@Failure行ではerrs.Errorではなくbrickserrs.Errorを使用します
  11. コメント禁止:メソッド本体に冗長なコメントを追加しないでください
  12. 検証:生成後にmake lintとmake update-swaggerを実行してください

アンチパターン(絶対に避けること)

以下のパターンはコードベースで確認されており、繰り返してはいけません。

❌ response.NoContentの代わりに生のw.WriteHeaderを使用する
// BAD
w.WriteHeader(http.StatusNoContent)

// GOOD
response.NoContent(w)
❌ ハンドラーファイルでパッケージレベルの単独関数を定義する
// BAD — standalone functions at package level violate the no-standalone-functions rule
func isResourceEmpty(output usecase.ResourceGetOutput) bool {
    return output.Name == ""
}

func firstNonEmpty(values ...string) string { ... }

// GOOD — private methods on the handler struct
func (h *ResourceHandler) isResourceEmpty(output usecase.ResourceGetOutput) bool {
    return output.Name == ""
}

func (h *ResourceHandler) firstNonEmpty(values ...string) string { ... }
❌ 検証エラー/解析エラーで、errorHandlerの前にlogger呼び出しがない
// BAD — skips the logger
if parseErr != nil {
    h.errorHandler.Error(w, brickserrs.New("MODULE_90", "invalid id", http.StatusBadRequest, nil))
    return
}

// GOOD — always log before handling
if parseErr != nil {
    h.logger.Error("invalid resource id", logger.Error(parseErr))
    h.errorHandler.Error(w, parseErr)
    return
}
❌ ハンドラー本体で、エラーコードをハードコードしたbrickserrs.New()をインラインで使用する

エラーはモジュールのerrs/パッケージで定義する必要があります(go-errorスキルを使用)。ハンドラーメソッドの本体でその場しのぎに作成してはいけません。

// BAD — hardcoded error codes inline in handler logic
h.errorHandler.Error(w, brickserrs.New("CATALOG_90", "invalid category_id", http.StatusBadRequest, nil))

// GOOD — reference a named error from the module's errs/ package
h.errorHandler.Error(w, errs.ErrInvalidCategoryID)

例外は、URL パラメーターの解析に使用する非公開メソッドnewBadRequestErrorです。構造体のヘルパーメソッド内であれば、インラインで生成しても問題ありません。

❌ 構造体のフィールドやコンストラクターで具象ユースケース型を使用する
// BAD — concrete pointer types
type ResourceHandler struct {
    resourceCreateUseCase *usecase.ResourceCreateUseCase
}

// GOOD — generic ucdecorator interface
type ResourceHandler struct {
    resourceCreateUseCase ucdecorator.UseCase[usecase.ResourceCreateInput, usecase.ResourceCreateOutput]
}
❌ ハンドラーメソッド名にHandleプレフィックスがない
// BAD
func (h *ResourceHandler) ListResources(w http.ResponseWriter, r *http.Request) { ... }
func (h *ResourceHandler) CreateResource(w http.ResponseWriter, r *http.Request) { ... }

// GOOD
func (h *ResourceHandler) HandleListResources(w http.ResponseWriter, r *http.Request) { ... }
func (h *ResourceHandler) HandleCreateResource(w http.ResponseWriter, r *http.Request) { ... }
❌ Swaggerの@Failureアノテーションで誤ったエラー型を使用する
// BAD — errs.Error is not the correct reference
// @Failure		500	{object}	errs.Error	"Internal server error"

// GOOD — use the brickserrs alias
// @Failure		500	{object}	brickserrs.Error	"Internal server error"

ワークフロー

  1. ユースケースの依存関係(ucdecorator.UseCase[Input, Output]を使用)を持つハンドラー構造体を作成します
  2. コンストラクターNewResourceHandlerを実装します
  3. 上記のパターンに従い、Handleプレフィックスを付けたハンドラーメソッドを実装します
  4. パスパラメーターが必要な場合は、非公開のparseUintPathParamメソッドとnewBadRequestErrorメソッドを追加します
  5. すべてのメソッドにSwaggerアノテーションを追加します(@Failureではbrickserrs.Errorを使用)
  6. モジュールのfx.goにFxの配線を追加します
  7. make lintとmake nilawayを実行し、静的テストで検証します
  8. make update-swaggerを実行してSwaggerドキュメントを再生成します

コメント