【Go】net/http と httprouter でAPIサーバを実装する

## 概要

Goの net/http パッケージと httprouter パッケージでAPIサーバを実装します。

## net/http とは

net/http パッケージは，GoでHTTPクライアントとHTTPサーバを実装するためのパッケージです。今回の記事では，HTTPサーバを実装するために使用します。

https://pkg.go.dev/net/http

## httprouter とは

HTTPリクエストをルーティングするためのパッケージです。
このHTTPルータは，軽量でパフォーマンスが良いことが特徴で，ルーティング先の数が増加したり非常に長いパスのリクエストに対応するときでも，高パフォーマンスで処理できます。(READMEに書いてあった）

https://github.com/julienschmidt/httprouter

httprouterを使わないルーティング方法もありますが，本記事では，httprouterを使ってルーティングを実施します。


## 実装

APIサーバのサンプルを実装するために，本の情報を取得する以下の2種類のAPIを実装します。
(簡単のため，DBへのアクセスは行いません。)

- GET /books API
- GET /books/:id API

### 構成

- 構成はレイヤードアーキテクチャを意識している。ただし，DBアクセスする Infrastracture 層は作成しない。
- handler は User Interface 層でユーザからのAPIコールを処理する。
- usecase は Usecase 層(application 層)ユースケースを実現する(ここでは，本の一覧を取得するユースケース)。

```
/src
  ┣ /handler
  ┃   ┗ book_handler.go
  ┣ /usecase
  ┃   ┗ book_usecase.go
  ┗ main.go
```

### ユースケースの実装

まずは，本の情報を取得するユースケースを実装します。
デモのため，サンプルデータを返します。

```Go:src/usecase/book_usecase.go
package usecase

type Book struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

// デモ用サンプルデータ
var sampleBookList = []Book{
	{
		ID:   "1",
		Name: "Go言語プログラミングエッセンス",
	},
	{
		ID:   "2",
		Name: "Go言語によるWebアプリケーション開発",
	},
}

// GET /books 用のユースケース
func GetBookList() []Book {
	// デモ用データを返す
	return sampleBookList
}

// GET /books/:id 用のユースケース
func GetBook(id string) *Book {
	// IDで指定した本が存在した場合はその本を返す
	for _, book := range sampleBookList {
		if book.ID == id {
			return &book
		}
	}
	// 存在しなかった場合は nil を返す
	return nil
}

```

### ユーザインターフェースの実装

ユーザからAPIコールを受けユースケース層に渡す処理と，クライアントにデータを返す処理を実装します。
関数の引数には `http.ResponseWriter`・`*http.Request`・`httprouter.Params`を渡しています。

パスパラメータの取得には，引数の`httprouter.Params` を使うことができ，`params.ByName(<パラメータ名>)`で取得できます。
ちなみに，クエリパラメータは，`req.URL.Query().Get(<パラメータ名>)`で取得できます。

また，`json:"<tag>"`を指定することで，APIのレスポンスのフィールドを変更できます。タグを指定しなかった場合は，構造体のフィールドがそのままレスポンスのフィールドになります。

```Go:src/handler/book_handler.go
import (
	"encoding/json"
	"net/http"
	"demoapi/src/usecase"

	"github.com/julienschmidt/httprouter"
)

type ErrorResponse struct {
	Message string `json:"message"`
}

type BookListResponse struct {
	Books []BookResponse `json:"books"`
}

type BookResponse struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

// GET /books のリクエストを処理する
func GetBookList(writer http.ResponseWriter, req *http.Request, params httprouter.Params) {
	bookList := usecase.GetBookList()

	// レスポンスを生成する
	response := BookListResponse{Books: []BookResponse{}}
	for _, book := range bookList {
		response.Books = append(response.Books, BookResponse{ID: book.ID, Name: book.Name})
	}

	// レスポンスを encode する
	err := json.NewEncoder(writer).Encode(response)
	if err != nil {
		return
	}
}

// GET /books/:id のリクエストを処理する
func GetBook(writer http.ResponseWriter, req *http.Request, params httprouter.Params) {
	// Path パラメータのidを取得
	id := params.ByName("id")

	// ID が指定されていない場合は 404 Not Found を返す
	if id == "" {
		writer.WriteHeader(http.StatusNotFound)
		json.NewEncoder(writer).Encode(ErrorResponse{
			Message: http.StatusText(http.StatusNotFound),
		})
		return
	}

	book := usecase.GetBook(id)

	// 本が存在しない場合は 404 Not Found を返す
	if book == nil {
		writer.WriteHeader(http.StatusNotFound)
		json.NewEncoder(writer).Encode(ErrorResponse{
			Message: http.StatusText(http.StatusNotFound),
		})
		return
	}

	// レスポンスを生成する
	response := BookResponse{ID: book.ID, Name: book.Name}

	// レスポンスを encode する
	err := json.NewEncoder(writer).Encode(response)
	if err != nil {
		return
	}
}
```

### APIサーバの設定

`router.GET()` では，クライアントがアクセスするパスと実行される handler の対応付けを行います。
ポートは8080に設定しました。

```Go:src/main.go
import (
	"net/http"
	"demoapi/src/handler"

	"github.com/julienschmidt/httprouter"
)

func main() {
	router := httprouter.New()

    // ユーザが http://<domain>/ にアクセスすると，GetBookList が実行される
	router.GET("/", handler.GetBookList)
    // ユーザが http://<domain>/ にアクセスすると，GetBook が実行される
    router.GET("/books/:id", handler.GetBook)

	http.ListenAndServe(":8080", router)
}

```

## 実行

以下を実行します。これでAPIサーバが起動します。

```bash
go mod init
go mod tidy

go run main.go
```

localhost の 8080 番ポートにアクセスすると，レスポンスが返ってくるのが確認できました。

```bash:実行結果
$ curl -i http://localhost:8080/books

{"books":[{"id":"1","name":"Go言語プログラミングエッセンス"},{"id":"2","name":"Go言語によるWebアプリケーション開発"}]}

$ curl http://localhost:8080/books/1

{"id":"1","name":"Go言語プログラミングエッセンス"}

$ curl http://localhost:8080/books/2

{"id":"2","name":"Go言語によるWebアプリケーション開発"}

$ curl http://localhost:8080/books/3

{"message":"Not Found"}

```

### まとめ

net/http と httprouter でAPIサーバを実装してみました。
誰かの参考になれば幸いです。