go-chi-handler について
go-chi-handler は、Go バックエンド向けの Chi HTTP ハンドラーを生成するコーディングスキルです。DTO マッピング、エラー処理、Swagger アノテーション、Fx 接続の規約を定めています。
主な機能
- 01REST HTTP ハンドラーの生成
- 02リクエスト・レスポンス DTO のマッピング
- 03Swagger アノテーションの追加
- 04一貫したエラー処理
- 05Fx 接続手順の提示
活用シーン
- 01REST 操作用のハンドラー作成
- 02一覧・作成・更新・削除エンドポイントの実装
- 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 アノテーションのルール
- @Summary:操作の概要(例:「リソース一覧の取得」、「リソースの作成」)
- @Description:エンドポイントの動作の詳細な説明
- @Tags:リソース名の複数形(例:「リソース」、「連絡先」)
- @Accept:常に
json - @Produce:常に
json - @Security:認証が必要な場合は
BearerAuthを追加します - @Param:パスパラメーターとリクエストボディを定義します
- パスパラメーター:
@Param id path int true "Resource ID" - リクエストボディ:
@Param request body dto.CreateResourceRequest true "Resource data"
- パスパラメーター:
- @Success:ステータスコードとレスポンス型
- 200:
{object} response.Envelope[dto.ResourceResponse] - 201:
{object} response.Envelope[dto.CreateResourceResponse] - 204:コンテンツなし。説明文字列のみ
- 200:
- @Failure:一般的なエラー(404、422、500)には
{object} brickserrs.Errorを指定します(エイリアスに注意) - @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),
通常、ハンドラーはポートとして公開せず、ルーターに提供します。
重要なルール
- 単独関数は禁止:ファイルにメソッドを持つ構造体がある場合、パッケージレベルの単独関数は決して追加しないでください。すべてのヘルパーは構造体の非公開メソッドにしてください(例:
parseUintPathParam、newBadRequestError)。 - 構造体:すべてのユースケース、
response.ErrorHandler、logger.Loggerを含めてください - コンストラクター:ポインター
*ResourceHandlerを返す必要があります - コンテキスト:リクエストから必ず取得してください:
ctx := r.Context() - リクエストのデコード:
request.ReadJSON(w, r, &dto)を使用してください。先に変数を宣言してから、ReadJSONを呼び出します - URL パラメーター:
chi.URLParamとstrconv.ParseUintを使用する非公開のparseUintPathParamメソッドで解析してください - エラー処理:すべてのエラーについて、検証エラーや入力の解析エラーも含め、必ず
h.logger.Error(...)とh.errorHandler.Error(w, err)を呼び出してからreturnしてください - レスポンスのマッピング:ユースケースの出力をDTOにマッピングしてください。ユースケースの出力を直接返してはいけません
- 成功レスポンス:
- List/Get:
response.JSON(w, http.StatusOK, data, http.Header{}) - Create:
response.JSON(w, http.StatusCreated, data, http.Header{}) - Update/Delete:
response.NoContent(w)
- List/Get:
- Swagger:すべてのハンドラーメソッドに完全なSwaggerアノテーションを追加してください。
@Failure行ではerrs.Errorではなくbrickserrs.Errorを使用します - コメント禁止:メソッド本体に冗長なコメントを追加しないでください
- 検証:生成後に
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"
ワークフロー
- ユースケースの依存関係(
ucdecorator.UseCase[Input, Output]を使用)を持つハンドラー構造体を作成します - コンストラクター
NewResourceHandlerを実装します - 上記のパターンに従い、
Handleプレフィックスを付けたハンドラーメソッドを実装します - パスパラメーターが必要な場合は、非公開の
parseUintPathParamメソッドとnewBadRequestErrorメソッドを追加します - すべてのメソッドにSwaggerアノテーションを追加します(
@Failureではbrickserrs.Errorを使用) - モジュールの
fx.goにFxの配線を追加します make lintとmake nilawayを実行し、静的テストで検証しますmake update-swaggerを実行してSwaggerドキュメントを再生成します