Step4:出品APIを作る - ch0cobanana/mercari-build-training GitHub Wiki

1. APIを呼び出す

POSTリクエストの送信と修正

$ curl -X POST 'http://127.0.0.1:9000/items'

とすると、期待しているコール成功時の{"message": "item received: "}とは異なる、name is requiredというレスポンスが返ってくる。以下のようにコマンドを修正すると期待通りに。

$ curl \
  -X POST \
  --url 'http://localhost:9000/items' \
  -d 'name=jacket'

なぜか

func parseAddItemRequest(r *http.Request) (*AddItemRequest, error) {
	req := &AddItemRequest{
		Name: r.FormValue("name"),
		// STEP 4-2: add a category field
	}

	// STEP 4-4: add an image field

	// validate the request
	if req.Name == "" {
		return nil, errors.New("name is required")
	}

	// STEP 4-2: validate the category field
	// STEP 4-4: validate the image field
	return req, nil
}

func parseAddItemRequest内に、if req.Name == "" { return nil, errors.New("name is required") }という、nameが空であったときに"name is required"とエラーメッセージを返すように設定されている。

Point

  • GETとPOSTの違い
    • GET:データの取得のみ。リクエストのコンテンツを入れてはいけない
    • POST:リソースの作成。サーバーにデータを送信し、変化をもたらす。PUTメソッドと異なり、同じPOSTに成功すると、複数回の注文を行うような追加の効果が出る。
  • ブラウザでhttp://127.0.0.1:9000/items にアクセスしても {"message": "item received: "} が返ってこないのはなぜ?
    • アクセスしたときに返ってくるHTTPステータスコード:200
    • 考えられる理由:/items の GET リクエストが / の GET にリダイレクトされ、200 OK が返った?

server.go コード(

package app #このコードはappというパッケージ内に属することを示す。Goでは必ずpackageを指定する必要がある

import (
	"encoding/json" #データをjson形式に変換したり、JSONデータをGOの構造体に変換するために使用
	"errors" #エラーハンドリングのためにGo標準のerror型を作成するために使用
	"fmt" #フォーマットされた文字列を出力するために使用
	"log/slog" #ログを記録するためのライブラリ
	"net/http" #HTTPサーバーやクライアント機能を提供する標準ライブラリ
	"os" #環境変数の取得やファイル操作のためのライブラリ
	"path/filepath" #ファイルやディレクトリのパスをああ使うためのライブラリ
	"strings" #文字列操作のためのライブラリ
)

#Server構造体の定義
type Server struct {
	// Port is the port number to listen on.
	Port string 
	// ImageDirPath is the path to the directory storing images.
	ImageDirPath string
}

// Run is a method to start the server.
// This method returns 0 if the server started successfully, and 1 otherwise.
func (s Server) Run() int {
	// set up logger
	logger := slog.New(slog.NewJSONHandler(os.Stderr, nil)) #loggerを作成し、標準エラー出力(os.Stderr)にJSON形式でログを出力するよう設定
	slog.SetDefault(logger) #デフォルトのロガーを設定
	// STEP 4-6: set the log level to DEBUG
	slog.SetLogLoggerLevel(slog.LevelInfo) #ログレベルをINFOに設定

	// set up CORS settings
	frontURL, found := os.LookupEnv("FRONT_URL") #環境変数FRONT_URLを取得。
	if !found {
		frontURL = "http://localhost:3000"
	} #foundがfalseの場合、つまり環境変数が設定されていない場合、デフォルトでhttp://localhost:3000を使用

	// STEP 5-1: set up the database connection

	// set up handlers
	itemRepo := NewItemRepository() #NewItemRepositoryで新しいリポジトリインスタンスを作成
	h := &Handlers{imgDirPath: s.ImageDirPath, itemRepo: itemRepo} #Handlers構造体を作成。imgDirPathにs.ImageDirPathをセット(画像ディレクトリのパス)。itemRepoにitemRepoをセット(データ管理用リポジトリ)。


	// set up routes ルーティングの設定
	mux := http.NewServeMux() #新しいルーティング用のServeMuxインスタンスを作成
	mux.HandleFunc("GET /", h.Hello) #GET /にアクセスされたらHelloハンドラーを実行
	mux.HandleFunc("POST /items", h.AddItem) #POST /itemsにアクセスされたらAddItemハンドラーを実行
	mux.HandleFunc("GET /images/{filename}", h.GetImage) #GET /images/{filename}で画像を取得

	// start the server サーバーの起動
	slog.Info("http server started on", "port", s.Port) #サーバーが起動したことをログに記録。
	err := http.ListenAndServe(":"+s.Port, simpleCORSMiddleware(simpleLoggerMiddleware(mux), frontURL, []string{"GET", "HEAD", "POST", "OPTIONS"})) #s.Portで指定されたポートでHTTPサーバーを起動。simpleCORSMiddleware(mux), frontURL, ...)でCORSとロギングのミドルウェアを起動
	if err != nil {
		slog.Error("failed to start server: ", "error", err)
		return 1
	} #サーバーの起動に失敗した場合、エラーメッセージをログに出力し、1を返す

	return 0
}

type Handlers struct { #Handlersはリクエストを処理するハンドラーをまとめた構造体。
	// imgDirPath is the path to the directory storing images.
	imgDirPath string #画像が保存されているディレクトリのpzす
	itemRepo   ItemRepository #データ管理用のリポジトリ
}

type HelloResponse struct { #Helloハンドラーが返すレスポンスの構造体
	Message string `json:"message"` #Massage string は”Hello World"というメッセージを格納
}

// Hello is a handler to return a Hello, world! message for GET / .
func (s *Handlers) Hello(w http.ResponseWriter, r *http.Request) {
	resp := HelloResponse{Message: "Hello, world!"} #レスポンスデータを作成
	err := json.NewEncoder(w).Encode(resp) #レスポンスをjsonに変換して出力
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	} #エラーが発生したらhttp.Errorで500エラーを返す
}

2. 新しい商品を登録する

ゴール:POST /itemsのエンドポイント拡張とitemsに関連するデータの永続化

前提として、準備されているPOST /itemsのエンドポイントは name という情報を受け取ることが出来ます。 ここで、category という情報も受け取れるように変更を加えましょう。

  • name:商品の名前(string型)
  • category:商品のカテゴリ(string型)
type AddItemRequest struct { #AddItemRequestはPOST /itemsリクエストのデータを格納する構造体
	Name string `form:"name"` #フォームから送信された"name"フィールドの値を格納
        Category string `form:"category"`  #同じようにcategoryも追加
	Image []byte `form:"image"` // STEP 4-4: add an image field
}
type AddItemResponse struct { #AddItemResponseはAddItemハンドラーのレスポンスデータを格納する構造体
	Message string `json:"message"` #json形式でmessageフィールドとして出力される
}
⚠️ **GitHub.com Fallback** ⚠️