Next.js × Go のWebアプリをDocker上で作成してみませんか？ 〜バックエンド実装編〜


# はじめに
👋こんにちは！Web系の企業に勤務し、地方からフルリモート勤務をしている@takakouと申します🏙️✨

今回は、「**Next.js × Go のWebアプリをDocker上で作成してみませんか？ 〜バックエンド実装編〜**」というテーマで記事をシェアします！📝🐳

この記事を執筆しようと思った経緯としては、私自身が **Next.js** × **Go** × **Docker**の組み合わせでアプリを作成しようとした際、この技術スタックに関する情報が少なく、詰まることも多かったからです😫💡

そこで、同じように、この組み合わせの技術スタックに興味を持つ初学者の方々のために、Dockerを使用して環境を構築する方法を詳しくシェアしたいと思い、この連載を始めました。🌟

記事執筆は未熟者で、至らない点もあるかと思いますが、皆さんのコメントやフィードバックをお待ちしています！🚀💬
# 目次
<!-- タイトルとアンカー名を編集 -->
1. [前提条件 💡](#1-前提条件)
1. [対象者 👨](#2-対象者)
1. [動作環境 🖥️](#3-動作環境)
1. [技術選定 🛠️](#4-技術選定)
1. [実装手順 🚀](#5-実装手順)
1. [完成コード](#6-完成コード)
1. [動作検証](#7-動作検証)
1. [参考文献 📚](#8-参考文献)
1. [おわりに 🎉](#9-おわりに)
<!-- 各チャプター -->

# 1 前提条件
今回の記事の前提条件は、[前回](https://qiita.com/drafts/a01af0515f49e90bd05c/edit)、[前々回](https://qiita.com/takakou/items/4f8cd3686c7ec84c141d)で既に **Next.js×Goのモノレポ構成のDockerの環境**を構築できていることです。そちらの記事をご覧になっていただいた後に、こちらの記事を確認いただけると、記事の内容について深く理解できると思いますのでよろしくお願いいたします。

https://qiita.com/takakou/items/a01af0515f49e90bd05c

https://qiita.com/takakou/items/4f8cd3686c7ec84c141d


# 2 対象者

* ある程度のWebアプリケーションに関する知識がある人
* Dcokerを使ったことがある人
* 必要な知識は自分自身である程度調べられる人

:::note warn
今回の記事では**コードの説明の細い説明は基本的に行いません**。
気になることがある場合は、記事を調べたり、公式のリファレンスを参照したり、chatGPTに聞いてみたりして自分で解決していただくようにお願いいたします。

:::

<a id="#environment"></a>
# 3 動作環境


* PC : **MacBook Air(M1,2020)**

* RAM : **8GB**

* OS : **macOS Monterey(ver12.1)**
:::note warn
**環境上の注意点**
**Docker** が使える前提で始めていきます。
**Docker**が入っていない場合は、導入するようにお願いいたします。
**Docker Compose V2**を使っています。

:::

<a id="#technical"></a>
# 4 技術選定
### フロントエンド
* Language: **TypeScript**

* Library: **React**

* FW: **Next.js**

### バックエンド
* Language: **Go**

* FW: **Gin**

* ORM: **Ent**

* DB: **PostgreSQL**

:::note
**ポイント**
ORMとは何ぞや？という方向けにORMに関する外部の記事を貼っておきます。
:::
https://utouto97.hatenablog.com/entry/2021/07/21/224759


<a id="#exec"></a>
# 5 実装手順
こちらの章では実装手順について、***DB設定**、**CORS設定***、**認証基盤**、**本登録基盤**に分けて実装をしておきます。

:::note warn
**注意1**
ここから先に書いてあるコードは全て`main.go`に追記するものです。
前回の記事までで、作成してある`main.go`に追記する形で対応をお願いいたします。
**完成コード**が見たい人はそこまで飛ばしてください

:::

:::note warn
**注意2**
**セキュリティ(パスワードの暗号化、確認用パスワード、etc...)** は特に意識していません。
このコードのまま**本番環境**に出すのは控えてください。

:::

## DB設定
**DB**の設定をします。
**PostgreSQL**との接続をし、その後 **Auto migration**を実行させます。
わざわざ、PostgreSQLの中に入ってSQLを直打ちしなくても良いのでめちゃくちゃ便利です。
```
//PostgreSQLに接続
client, err := ent.Open("postgres", "host=db port=5432 user=postgres dbname=bookers2 password=password sslmode=disable")

if err != nil {
        log.Fatalf("failed opening connection to postgres: %v", err)
}
// 自動のmigrationを行う
if err := client.Schema.Create(context.Background()); err != nil {
        log.Fatalf("failed creating schema resources: %v", err)
}
```

## CORS設定
**CORS**の設定をします。

:::note warn
**注意点**
今回は面倒なので、リクエストを許可するoriginを`locahost:3000`とハードコーディングしていますが、本来なら、環境変数などにすべきなので、実際にデプロイをするときなどは気をつけてください。
:::
```
// CORS設定
router.Use(func(c *gin.Context) {
    c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
    c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
    c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
    c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
    if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(200)
            return
    }
    c.Next()
})
```


## 認証基盤作成
`main.go`に記述を追加していく形で対応をお願いいたします。
ここからは順不同です。
### 登録
```diff_go:main.go
// ユーザ新規登録機能
router.POST("users/sign_up",func(c *gin.Context){

    // サインアップで送られてくるリクエストを型定義
    type SignUpRequest struct{
        Email string `json:"email" binding:"required"`
        Name string `json:"name" binding:"required"`
        Password string `json:"password" binding:"required"`
    }

    // 変数reqをSignUpRequestで定義
    var req SignUpRequest

    //reqに取得したデータを格納、変換でエラーが起きた場合はエラーを返して終了
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": "Invalid request"})
        return
    }

    // ユーザ登録を行う
    newUser,err := client.User.
    Create().
    SetEmail(req.Email).
    SetName(req.Name).
    SetPassword(req.Password).
    Save(context.Background())

    // エラーの場合はエラーを返して処理終了。
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error(),"messsage":"sign up missing"})
        return
    }
    // 保存したUserの情報をレスポンスとして返す。
    c.JSON(201, gin.H{"user": newUser})

})
```


### ログイン

:::note warn
ログイン機能とは言いつつも、リクエスト内に含まれた認証情報に対して `true` or `false`を返すだけです。
**セッション管理**などはフロントエンド側で実装をします。
:::
```diff_go:main.go
import(
    ....
    // `user`のメソッドを使うため
    "Next_Go_App/ent/user"
    ....
)

//ユーザログイン機能
router.POST("users/sign_in",func(c *gin.Context){

    // ログインで送られてくるリクエストを型定義
    type SignInRequest struct{
        Email string `json:"email" binding:"required"`
        Password string `json:"password" binding:"required"`
    }

    // 変数reqをSignInRequestで定義
    var req SignInRequest

    //reqに取得したデータを格納、変換でエラーが起きた場合はエラーを返して終了
    if err :=c.ShouldBindJSON(&req); err != nil{
        c.JSON(400, gin.H{"error": "Invalid request"})
        return
    }

    // ユーザの検索、パスワードの照合を行う
    sign_in_user, err := client.User.Query().
        Where(user.EmailEQ(req.Email), user.PasswordEQ(req.Password)).
        First(context.Background())
    
    //エラーを返す
    if err != nil {
        c.JSON(401, gin.H{"error": "invalid credentials"})
        return
    }

    // ログイン成功
    c.JSON(200, gin.H{"user": sign_in_user})

})

```

## 本管理基盤作成
本のCRUDについて順番に記述していきます。

### 登録
```diff_go:main.go
// 本の新規登録
router.POST("/books",func(c *gin.Context){

    // 本の新規登録で送られてくるリクエストを型定義
    type  NewBookRequest struct{
        Title string `json:"title" binding:"required"`
        Body string `json:"body" binding:"required"`
        UserId int`json:"user_id" binding:"required"`
    }

    // reqをNewBookRequestで定義
    var req NewBookRequest
    if err:=c.ShouldBindJSON(&req); err!=nil{
        c.JSON(400,gin.H{"error":"Invalid request"})
        return
    }

    // 本の情報を保存
    newBook,err:=client.Book.
            Create().
            SetTitle(req.Title).
            SetBody(req.Body).
            SetUserID(req.UserId).
            Save(context.Background())
    
    // エラーがある場合はエラーを返して終了
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error(),"message":"create book missing"})
        return
    }

    // 保存したBookの情報をレスポンスとして返す
    c.JSON(201, newBook)
})

```
### 一覧
```diff_go:main.go
// 本の一覧を取得
router.GET("/books",func(c *gin.Context){

    // Book一覧を取得する
    books,err:=client.Book.Query().All(context.Background())

    // エラーならエラーを返して終了
    if err!=nil{
        c.JSON(500,gin.H{"error": err.Error(),"message":"Could not get the book list."})
        return
    }

    // booksをjson形式で返す
    c.JSON(200, books)
})

```
### 詳細
```diff_go:main.go
import(
    ....
    //文字列と数値の相互変換用
	"strconv"
    ....
)

// 本の情報を取得
router.GET("/books/:id",func(c *gin.Context){

    // URLパラメータから本のIDを取得する。
    bookIDStr:=c.Param("id")

    // 文字->数字変換
    bookID,err :=strconv.Atoi(bookIDStr)

    // パラメータが不正な場合はエラーを出力して終了
    if err != nil {
        c.JSON(400,gin.H{"error": "Invalid Book ID"})
        return
    }
    
    // 指定されたIDの本をデータベースから検索
    book, err := client.Book.Get(context.Background(), bookID)

    // 本が見つからない場合はエラーを返して終了
    if err != nil {
        c.JSON(404,gin.H{"error": err.Error(),"message":"Book with specified id not found"})
        return
    }

    // 検索した本の情報をJSON形式でレスポンスとして返す
    c.JSON(200, book)

})
```
### 更新
```diff_go:main.go
//本情報を更新する。
router.PATCH("/books/:id",func(c *gin.Context){

    // 本の新規登録で送られてくるリクエストを型定義
    type  UpdateBookRequest struct{
        Title string `json:"title" binding:"required"`
        Body string `json:"body" binding:"required"`
        UserId int`json:"user_id"`
    }
    
    // 引数で値を受け取るように変数を定義
    var book UpdateBookRequest

    // bookに受け取った値を格納
    if err:=c.ShouldBindJSON(&book);err!=nil{
        c.JSON(400,gin.H{"error": err.Error(),"message":"Invalid Book columnns"})
        return
    }
    
    // URLパラメータから本のIDを取得
    bookIDStr:=c.Param("id")

    // 数値に変換
    bookID,err:=strconv.Atoi(bookIDStr)

    // パラメータが不正な場合はエラーを返して終了
    if err!=nil{
        c.JSON(400,gin.H{"error": err.Error(),"message":"could not translation string->int"})
        return
    }

    // 指定されたIDの本をデータベースから検索
    search_book, err := client.Book.Get(context.Background(), bookID)
    if err != nil {
        c.JSON(404, gin.H{"error": err.Error(), "message": "Book not found"})
        return
    }

    // UserIdの確認
    if *search_book.UserID != book.UserId{
        c.JSON(403, gin.H{"error": "Unauthorized", "message": "UserId does not match"})
        return
    }

    // 指定されたIDの本をデータベースから検索、更新
    update_book, err := client.Book.
            UpdateOneID(bookID).
            SetTitle(book.Title).
            SetBody(book.Body).
            Save(context.Background())
    
    // エラーならエラーを返して終了
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error(),"message":"Couldn't update"})
        return
    }

    // 本の情報をJSON形式でレスポンスとして返す
    c.JSON(200, update_book)
})

```
### 削除
```diff_go:main.go
//本を削除
router.DELETE("/books/:id",func(c *gin.Context){
    // URLパラメータから本のIDを取得する
    bookIDStr := c.Param("id")
    bookID, err := strconv.Atoi(bookIDStr)
    if err != nil {
            c.JSON(400, gin.H{"error": "無効な本のIDです。"})
            return
    }
    
    // 指定されたIDの本をデータベースから削除する
    err = client.Book.DeleteOneID(bookID).Exec(context.Background())
    
    
    if err != nil {
        c.JSON(404, gin.H{"error": "削除に失敗しました。"})
        return
    }
    
    c.JSON(200, gin.H{"message": "削除完了しました。"})
})


```
# 6 完成コード(例) 

:::note warn
**警告**
正直言ってかなり可読性が悪い書き方をしています。
可読性を上げるために関数化などをしたり、型宣言を先にしたりと最初はしていたのですが、それだと初学者向けではない...と思い今の形になっています。
コードが動いた後に、各自で、効率的な書き方を試してみていただけますと幸いです。
:::
```diff_go:main.go
package main

import (
	"github.com/gin-gonic/gin"
	"context"
  	"log"
	"Next_Go_App/ent"
	"Next_Go_App/ent/user"
	_ "github.com/lib/pq"
	"strconv"
)
func main() {

	// PostgreSQLに接続
	client, err := ent.Open("postgres", "host=db port=5432 user=postgres dbname=bookers2 password=password sslmode=disable")

	// DB接続でエラーがあった場合にエラーを出力
	if err != nil {
		log.Fatalf("failed opening connection to postgres: %v", err)
	}
	// 自動でマイグレーション & エラーがあった場合にエラーを出力
	if err := client.Schema.Create(context.Background()); err != nil {
		log.Fatalf("failed creating schema resources: %v", err)
	}

	// Ginフレームワークのデフォルトの設定を使用してルータを作成
	router := gin.Default()

	// CORS設定
	router.Use(func(c *gin.Context) {
		c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
		c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
		c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
		if c.Request.Method == "OPTIONS" {
			c.AbortWithStatus(200)
			return
		}
		c.Next()
	})
	
	// ルートハンドラの定義
	router.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "Hello, Bookers!",
		})
	})

	// ユーザ新規登録機能
	router.POST("users/sign_up",func(c *gin.Context){

		// サインアップで送られてくるリクエストを型定義
		type SignUpRequest struct{
			Email string `json:"email" binding:"required"`
			Name string `json:"name" binding:"required"`
			Password string `json:"password" binding:"required"`
		}

		// 変数reqをSignUpRequestで定義
		var req SignUpRequest

		//reqに取得したデータを格納、変換でエラーが起きた場合はエラーを返して終了
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(400, gin.H{"error": "Invalid request"})
			return
		}

		// ユーザ登録を行う
		newUser,err := client.User.
        Create().
        SetEmail(req.Email).
        SetName(req.Name).
        SetPassword(req.Password).
        Save(context.Background())

		// エラーの場合はエラーを返して処理終了。
		if err != nil {
			c.JSON(500, gin.H{"error": err.Error(),"messsage":"sign up missing"})
			return
		}
		// 保存したUserの情報をレスポンスとして返す。
		c.JSON(201, gin.H{"user": newUser})

	})

	// ユーザログイン機能
	router.POST("users/sign_in",func(c *gin.Context){

		// ログインで送られてくるリクエストを型定義
		type SignInRequest struct{
			Email string `json:"email" binding:"required"`
			Password string `json:"password" binding:"required"`
		}

		// 変数reqをSignInRequestで定義
		var req SignInRequest

		//reqに取得したデータを格納、変換でエラーが起きた場合はエラーを返して終了
		if err :=c.ShouldBindJSON(&req); err != nil{
			c.JSON(400, gin.H{"error": "Invalid request"})
			return
		}

		// ユーザの検索を行う
		sign_in_user, err := client.User.Query().
			Where(user.EmailEQ(req.Email), user.PasswordEQ(req.Password)).
			First(context.Background())
		
		//エラーを返す
		if err != nil {
			c.JSON(401, gin.H{"error": "invalid credentials"})
			return
		}

		// ログイン成功
		c.JSON(200, gin.H{"user": sign_in_user})

	})


	// 本の新規登録
	router.POST("/books",func(c *gin.Context){

		// 本の新規登録で送られてくるリクエストを型定義
		type  NewBookRequest struct{
			Title string `json:"title" binding:"required"`
			Body string `json:"body" binding:"required"`
			UserId int`json:"user_id" binding:"required"`
		}

		// reqをNewBookRequestで定義
		var req NewBookRequest
		if err:=c.ShouldBindJSON(&req); err!=nil{
			c.JSON(400,gin.H{"error":"Invalid request"})
			return
		}

		// 本の情報を保存
		newBook,err:=client.Book.
				Create().
				SetTitle(req.Title).
				SetBody(req.Body).
				SetUserID(req.UserId).
				Save(context.Background())
		
		// エラーがある場合はエラーを返して終了
		if err != nil {
			c.JSON(500, gin.H{"error": err.Error(),"message":"create book missing"})
			return
		}
	
		// 保存したBookの情報をレスポンスとして返す
		c.JSON(201, newBook)
	})


	// 本の一覧を取得
	router.GET("/books",func(c *gin.Context){

		// Book一覧を取得する
		books,err:=client.Book.Query().All(context.Background())

		// エラーならエラーを返して終了
		if err!=nil{
			c.JSON(500,gin.H{"error": err.Error(),"message":"Could not get the book list."})
			return
		}

		// booksをjson形式で返す
		c.JSON(200, books)
	})

    // 本の情報を取得
	router.GET("/books/:id",func(c *gin.Context){

		// URLパラメータから本のIDを取得する。
		bookIDStr:=c.Param("id")

		// 文字->数字変換
		bookID,err :=strconv.Atoi(bookIDStr)

		// パラメータが不正な場合はエラーを出力して終了
		if err != nil {
			c.JSON(400,gin.H{"error": "Invalid Book ID"})
			return
		}
		
		// 指定されたIDの本をデータベースから検索
		book, err := client.Book.Get(context.Background(), bookID)

		// 本が見つからない場合はエラーを返して終了
		if err != nil {
			c.JSON(404,gin.H{"error": err.Error(),"message":"Book with specified id not found"})
			return
		}

		// 検索した本の情報をJSON形式でレスポンスとして返す
		c.JSON(200, book)

	})


	// 本情報を更新する。
	router.PATCH("/books/:id",func(c *gin.Context){

		// 本の新規登録で送られてくるリクエストを型定義
		type  UpdateBookRequest struct{
			Title string `json:"title" binding:"required"`
			Body string `json:"body" binding:"required"`
			UserId int`json:"user_id" binding:"required"`
		}
		
		// 引数で値を受け取るように変数を定義
		var book UpdateBookRequest
	
		// bookに受け取った値を格納
		if err:=c.ShouldBindJSON(&book);err!=nil{
			c.JSON(400,gin.H{"error": err.Error(),"message":"Invalid Book ID"})
			return
		}
		
		// URLパラメータから本のIDを取得
		bookIDStr:=c.Param("id")

		// 数値に変換
		bookID,err:=strconv.Atoi(bookIDStr)

		// パラメータが不正な場合はエラーを返して終了
		if err!=nil{
			c.JSON(400,gin.H{"error": err.Error(),"message":"could not translation string->int"})
			return
		}
		// 指定されたIDの本をデータベースから検索、更新
		update_book, err := client.Book.
				UpdateOneID(bookID).
				SetTitle(book.Title).
				SetBody(book.Body).
				Save(context.Background())
		
		// エラーならエラーを返して終了
		if err != nil {
			c.JSON(404, gin.H{"error": err.Error(),"message":"Couldn't update"})
			return
		}
	
		// 本の情報をJSON形式でレスポンスとして返す
		c.JSON(200, update_book)
	})

	// 本を削除
	router.DELETE("/books/:id",func(c *gin.Context){
		// URLパラメータから本のIDを取得する
		bookIDStr := c.Param("id")

		// 数値に変換
		bookID, err := strconv.Atoi(bookIDStr)

		// パラメータが不正な場合はエラーを返して終了
		if err != nil {
			c.JSON(400, gin.H{"error": "Invalid Book ID"})
			return
		}

		// 指定されたIDの本をデータベースから検索、削除
		err = client.Book.DeleteOneID(bookID).Exec(context.Background())

		// エラーが起きた場合はエラーをあえして終了
		if err != nil {
			c.JSON(404, gin.H{"error": "Failed to delete"})
			return
		}

		c.JSON(200, gin.H{"message": "Delete completed"})
	})
		
	// サーバーの開始
	router.Run(":8080")
}



```

# 7 動作検証
動作検証では**Talend API**を使用します。
**Talend API**の導入、使い方についてはわかりやすくまとめられている記事がありましたので共有をしておきます。

https://qiita.com/ktr1211/items/c16cb4f99f91b998af46

コンテナを起動していない場合は下記コマンドで起動をしておきましょう
```
~ Next_GO_App $ docker compose up backend 
```

## 認証基盤
### 登録
ユーザのデータを登録してみます。
- リクエストは、`localhost:8080/users/sign_up`に**POST**メソッドで送ります。
- リクエストボディは、`{
	"email" : "user@user.com",
	"name" : "user",
    "password" : "password"
}`で送ります
- valueは別に指定はないので好きなものに変えてもらって大丈夫です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/df491c2b-311f-aaed-cba8-9a5f79e6c53f.png)
右上の「**Send**ボタン」を押してリクエストを送ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/483b2232-fd9c-7193-9eee-7f50b369810d.png)
このようにレスポンスが返ってきたら問題なしです。

### ログイン

先ほど作成したユーザで認証ができるか試します。
リクエストは、`localhost:8080/users/sign_in`にPOSTメソッドで送ります。
- リクエストボディは、`{
	"email" : "user@user.com",
    "password" : "password"
}`で送ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/70ec2c03-e7a5-ccaa-01ce-6dbb4c05efae.png)
右上の「**Send**ボタン」を押してリクエストを送ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/6ee2d03b-ba34-6347-ae7b-589b9903ca21.png)
認証が通ればこんな感じでレスポンスが返ってきます。
逆に通らない場合も試してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/2205577d-1152-cc62-9bab-61c407cfd709.png)
パスワードが間違っていたり、ユーザが存在しなかったりするとこんな感じでエラーが返ってきます。

## 本管理基盤
:::note warn
**注意**
こちらの章では、エラーが起きた際の再現はしません。
自身でエラーを再現してみてください。
:::
### 登録
本のデータを登録してみます。
- リクエストは、`localhost:8080/books`に**POST**メソッドで送ります。
- リクエストボディは、`{
    "title" : "title1",
    "body" : "body1",
    "user_id": "1"
}`で送ります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/bcf911c8-f577-9ffb-a2a6-d8185c1b7dda.png)
こんな感じでレスポンスが返ってくれば問題ないです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/ab9eaccb-f121-f2a1-6622-d885ca02751e.png)


### 一覧
本の一覧を取得します。
- リクエストは、`localhost:8080/books`に**GET**メソッドで送ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/dc51d64c-d688-cf74-ccc7-b2b4f8319394.png)
こんな感じでレスポンスが返ってくれば問題ないです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/1ff000e9-39b3-4aa9-347e-453c34c8009f.png)


### 詳細
本の詳細を取得します。
- リクエストは、`localhost:8080/books/1`に**GET**メソッドで送ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/f55abed8-7784-daa9-3588-ce9d54f69b04.png)
レスポンスがこんな感じで返ってきたら問題ないです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/06744a47-eb1c-cc83-ec3f-7987e9fb1bd5.png)


### 更新
本のデータを更新してみます。
- リクエストは、`localhost:8080/books/1`に**PATCH**メソッドで送ります。
- リクエストボディは、`{
    "title" : "title2",
    "body" : "body1",
    "user_id": "1"
}`で送ります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/b817afbd-b4b3-1757-d43b-bc3cb384aeee.png)
こんな感じで更新後のデータが帰ってくれば問題なしです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/8120b6a4-6749-0290-9b03-856b1e8466ea.png)


### 削除
- リクエストは、`localhost:8080/books/1`に**DELETE**メソッドで送ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/2421f9cd-22b2-f8a9-e77c-d8ea199c77a6.png)
レスポンスがこんな感じで返ってきたら問題なしです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/2c95b120-7b09-90dc-7d5f-6871eac4de0a.png)


# 8 参考文献

https://entgo.io/ja/docs/getting-started/]

https://gin-gonic.com/ja/

# 9 おわりに

今回の記事では、バックエンドの実装方法について説明をしました。

本来なら説明すべき、説明したいところもたくさんあったのですが、あまりに長くなってしまいそうなのでかなり省略しています🙏

今は chatGPTなどのAIツールが優秀なのでわからないことがあればそちらに聞いてみることをお勧めします。

次回の記事では、フロントエンドの実装について説明をしていきます!(年内には出したいという気持ち....)

この連載を通じて、より深い理解とスキルの向上を目指して、一緒に学んでいただけると幸いです🌱👨‍🎓

(最後に、気づいた方もいらっしゃると思うのですが、絵文字を入れるのは苦手なので、文章に沿った絵文字を挿入するのに、ChatGPTを利用させていただきましたが、いかがでしたでしょうか。不快に思われた方がいたら申し訳ございません🙇‍♂️)