Go × Gin でMVC構成のブログ記事投稿用Web APIを構築する:基礎からスケーラブル設計まで

  • golang
    golang
  • gin
    gin
2023/12/03に公開

はじめに

Go言語でWeb APIを作るにあたって、Gin は最も人気のある軽量フレームワークのひとつです。シンプルな構文と高速なパフォーマンスを備えつつ、柔軟なルーティングやミドルウェアの仕組みも提供しており、小規模なプロトタイピングから中〜大規模なサービス開発まで対応できます。

ただし、コードが増えるにつれて「どこに何を書けばいいか」が分かりづらくなったり、修正時の影響範囲が大きくなったりすることもあります。

そこで本記事では、以下を意識しながら進めていきます:

  • MVC(Model, View, Controller)構成での整理されたコード設計
  • 小さく始めてスケールできるディレクトリ設計
  • Ginの基本機能を活用しつつ実務でも通用する構成へと育てる方針

本記事でできるようになること

  • Ginを使って最小限のAPIサーバーを立ち上げられる
  • MVC構成の基本と、各レイヤーの責務が理解できる
  • スケーラブルな設計を見据えた構成を自分で組めるようになる

また、このシリーズは全3回構成となっており、次回以降はテストやCI導入といった 本番運用を見据えた実践ノウハウ にも踏み込んでいきます。

https://shinagawa-web.com/blogs/go-gin-mvc-testing

https://shinagawa-web.com/blogs/go-gin-production-ready

まずは、APIサーバーの構築を土台から固めていきましょう。

今回作成したコードは下記のリポジトリに載せています。

https://github.com/shinagawa-web/go-gin-blog-api

ディレクトリ構成とファイル配置

今回は「ブログ記事の投稿・取得」を想定したWeb APIを題材に、実務にも応用しやすい MVC構成(Model-View-Controller) をベースに構成していきます。

go-gin-blog-api/
├── main.go                     // アプリのエントリーポイント
├── handler/                   // HTTPハンドラ(Controller相当)
│   └── post_handler.go
├── service/                   // ビジネスロジック層(Service)
│   └── post_service.go
├── repository/                // データアクセス層(Repository)
│   └── post_repository.go
├── model/                     // データモデル定義(Model)
│   └── post.go
└── go.mod                     // Goモジュール設定

各ファイルの役割

  • main.go
    アプリの起動処理やルーティング設定を行います。全体の初期化処理のエントリーポイントです。

  • handler/post_handler.go
    ルーティングに紐づくHTTPリクエストの処理を記述します。例:記事投稿、記事一覧取得など。

  • service/post_service.go
    ビジネスロジックを記述します。たとえば「記事投稿時のバリデーション」や「公開・非公開の切り替え処理」などがここに含まれます。

  • repository/post_repository.go
    データの取得・保存など、ストレージに関する処理を担当します。現在はDB未導入のためインメモリで管理し、あとで差し替え可能なように抽象化します。

  • model/post.go
    Post という構造体を定義し、全層で共通して扱うデータモデルを明確化します。

作る機能(予定)

  • 記事の投稿(POST /posts
  • 記事の一覧取得(GET /posts
  • 記事の詳細取得(GET /posts/:id

必要に応じて、編集・削除・下書き保存・公開状態切替 なども追加可能です。

この構成をベースに、次は main.go を作成してサーバー起動とルーティングを設定していきます。
引き続き、"シンプルだけど拡張しやすい設計" を意識して進めていきます。

最小限のGinサーバーを立ち上げる

まずは、Ginを使って最小限のWebサーバーを動かしてみます。
いきなりフル機能を目指すのではなく、「とにかく動く状態」を確認しておくと、後の構築がスムーズになります。

GoとGinのインストール

プロジェクトディレクトリを作成し、Goモジュールを初期化します。

mkdir go-gin-blog-api
cd go-gin-blog-api
go mod init go-gin-blog-api

Ginをインストール

go get github.com/gin-gonic/gin

以下のコードを main.go に記述します。

package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/health", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"status": "ok",
		})
	})

	r.Run(":8080") // ポート8080で起動
}

サーバーを起動

go run main.go

起動後、http://localhost:8080/health にアクセスすると、次のようなJSONレスポンスが返ります。

{"status":"ok"}

Image from Gyazo

補足:なぜ /health を使うのか?

このようなエンドポイントは「ヘルスチェック用」として利用されることが多く、本番環境でもAPIの起動状態を外部から確認するのに役立ちます。

Airの導入でホットリロードを実現する

開発中にコードを変更するたびに go run main.go を手動で再実行するのは地味に面倒です。
この作業を自動化してくれる便利ツールが air です。Air を使えば、コード変更時に自動で再ビルド&再起動してくれるようになります。

以下のコマンドで air をインストールします。

go install github.com/air-verse/air@latest

インストール後、$GOPATH/bin にバイナリが追加されます。パスが通っていない場合は .zshrc または .bashrc に以下を追加してください:

export PATH="$(go env GOPATH)/bin:$PATH"

ターミナルを再起動、または source ~/.zshrc 等で再読み込み後、以下で確認できます。

air -v

air.air.toml という設定ファイルを利用できます。プロジェクトルートに以下の内容で作成します。

.air.toml
root = "."
tmp_dir = "tmp"

[build]
  cmd = "go build -o ./tmp/main main.go"
  bin = "tmp/main"
  full_bin = "tmp/main"
  include_ext = ["go"]
  exclude_dir = ["vendor"]
  exclude_file = []
  delay = 1000
  stop_on_error = true

[log]
  time = true

[color]
  main = "yellow"

ポイント

  • tmp_dir に一時的にビルドされた実行ファイルを出力
  • build.cmd で実行バイナリをビルド
  • bin に指定したファイルが毎回実行される(通常の go run 相当)

以下のコマンドを実行すると、air がソースコードの変更を監視し、即座に再起動してくれます。

air

Image from Gyazo

動作確認

ファイルを編集して保存し、ターミナルに次のようなログが出れば成功です。

main.go has changed
Restarting
Running tmp/main

Image from Gyazo

補足:なぜ tmp/main なのか?

Airは内部で go build を行ってバイナリを生成し、それを実行します。その出力先が tmp/main です。これにより、go run よりも高速な再起動が実現されます。

Airの導入により、開発効率が大幅に向上します。以降はコードを編集して保存するだけでサーバーが自動再起動され、動作確認のサイクルが非常に快適になります。

次は、ブログ記事データを扱うモデルを定義し、APIを徐々に実装していきます。ステップごとに整理して進めていきましょう。

ブログ記事モデル(Post)の定義

APIを設計するうえで、まずは扱うデータ構造を明確にしておくことが大切です。今回はブログ記事を投稿・取得する機能を作っていくため、Post という構造体を定義します。

model/post.go に以下のようなコードを記述します。

model/post.go
package model

type Post struct {
	ID      string `json:"id"`      // 記事ID
	Title   string `json:"title"`   // タイトル
	Content string `json:"content"` // 本文
	Author  string `json:"author"`  // 執筆者名
}

この構造体は、今後のサービス層やハンドラ層でも共通して使用されます。

JSONタグの意味

各フィールドに付けている json:"..." タグは、GinがリクエストやレスポンスをJSONとしてシリアライズ・デシリアライズする際に使う名前です。

たとえば、以下のようなJSONが /posts エンドポイントに送られたときに、この構造体にマッピングされます

{
  "id": "1",
  "title": "Getting Started with Gin",
  "content": "Gin is a lightweight web framework for Go.",
  "author": "Gopher"
}

この Post モデルを中心に、次は「記事の投稿」と「記事一覧取得」のAPIルーティングを設計し、Ginのハンドラ層を実装していきます。

記事の作成と取得:ハンドラの実装

Post モデルを使って、実際にブログ記事を登録・取得するエンドポイントを実装していきます。
ここでは以下の2つの処理を担当するハンドラを作ります。

  • POST /posts:新しい記事の投稿
  • GET /posts:記事一覧の取得

handler/post_handler.go に以下のようなコードを記述します。

handler/post_handler.go
package handler

import (
	"go-gin-blog-api/model"
	"net/http"
	"sync"

	"github.com/gin-gonic/gin"
)

// 簡易的なメモリ保存(本来はDBを使う)
var (
	posts = []model.Post{}
	mutex = &sync.Mutex{}
)

func RegisterPostRoutes(r *gin.Engine) {
	r.POST("/posts", CreatePost)
	r.GET("/posts", GetPosts)
}

// 記事の投稿ハンドラ
func CreatePost(c *gin.Context) {
	var newPost model.Post
	if err := c.ShouldBindJSON(&newPost); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	mutex.Lock()
	posts = append(posts, newPost)
	mutex.Unlock()

	c.JSON(http.StatusCreated, newPost)
}

// 記事一覧取得ハンドラ
func GetPosts(c *gin.Context) {
	mutex.Lock()
	defer mutex.Unlock()

	c.JSON(http.StatusOK, posts)
}

コード補足

このハンドラは、DBを使わずにメモリ上で記事の投稿と一覧取得ができる簡易的な実装です。

var (
    posts = []model.Post{}
    mutex = &sync.Mutex{}
)
  • posts:投稿データを保持するスライスです。実際のアプリではDBで管理しますが、ここではメモリ上で管理しています。
  • mutex:複数のリクエストが同時にアクセスしてもデータが壊れないように、排他制御を行います。
func CreatePost(c *gin.Context) {
    var newPost model.Post
    if err := c.ShouldBindJSON(&newPost); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    mutex.Lock()
    posts = append(posts, newPost)
    mutex.Unlock()

    c.JSON(http.StatusCreated, newPost)
}
  • c.ShouldBindJSON(&newPost):POSTされたJSONリクエストを model.Post にバインドします。
  • mutex.Lock()mutex.Unlock():他のリクエストと同時に posts を書き換えないようにロックします。
  • 成功すれば 201 Created ステータスで投稿内容を返します。
func GetPosts(c *gin.Context) {
    mutex.Lock()
    defer mutex.Unlock()

    c.JSON(http.StatusOK, posts)
}
  • 投稿データ posts をそのままJSONで返すだけのシンプルな実装です。
  • 読み取り時もロックをかけることで、整合性のある状態が保たれます。

これは後でサービス層・リポジトリ層に分けていく予定です。

main.go にルーティングを追加

main.go
package main

import (
+	"github.com/gin-gonic/gin"
	"go-gin-blog-api/handler"
)

func main() {
	r := gin.Default()

	r.GET("/healthz", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"status": "ok",
		})
	})

+	handler.RegisterPostRoutes(r)

	r.Run(":8080") // ポート8080で起動
}

動作確認

記事の投稿リクエスト(POST /posts)

curl -X POST http://localhost:8080/posts \
  -H "Content-Type: application/json" \
  -d '{
    "id": "1",
    "title": "Getting Started with Gin",
    "content": "Gin is a lightweight web framework for Go.",
    "author": "Gopher"
}'

Image from Gyazo

記事の取得リクエスト(GET /posts)

curl http://localhost:8080/posts

Image from Gyazo

補足:永続化はまだ不要

この段階ではDBを使わず、メモリ上に保持するだけの簡易実装としています。
そのためサーバーを再起動すると投稿した内容は消えます。
本格的な永続化(RDBなど)への切り替えは後ほど扱います。

サービス層の導入でビジネスロジックを分離する

アプリが成長するにつれ、ルーティングとビジネスロジックが密結合していると、保守性が低下します。
そのため、ビジネスロジックをサービス層に分離して、責務を整理しておくのが重要です。

ここでは、記事の作成・取得処理をサービスに切り出し、ハンドラから呼び出す構成にリファクタリングしていきます。

service/post_service.go に以下のようなコードを記述します。

service/post_service.go
package service

import (
	"go-gin-blog-api/model"
	"sync"
)

type PostService interface {
	Create(post model.Post) model.Post
	List() []model.Post
}

type postService struct {
	mu    sync.Mutex
	posts []model.Post
}

func NewPostService() PostService {
	return &postService{
		posts: make([]model.Post, 0),
	}
}

func (s *postService) Create(post model.Post) model.Post {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.posts = append(s.posts, post)
	return post
}

func (s *postService) List() []model.Post {
	s.mu.Lock()
	defer s.mu.Unlock()

	return append([]model.Post(nil), s.posts...) 
}

コード解説

type PostService interface {
	Create(post model.Post) model.Post
	List() []model.Post
}
  • PostServiceインターフェース:サービス層が提供する機能(=契約)を定義します。
  • テストや将来的な実装の差し替えがしやすくなります(依存の抽象化)。
type postService struct {
	mu    sync.Mutex
	posts []model.Post
}
  • postService構造体:実際の実装です。
  • posts: 投稿一覧を保持(今回はDBではなくメモリ)。
  • mu: 複数リクエストからの同時アクセスに備えたミューテックス(排他制御)です。
func (s *postService) Create(post model.Post) model.Post {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.posts = append(s.posts, post)
	return post
}
  • 投稿をスライスに追加しています。
  • LockUnlock の流れで スレッドセーフ(並列でも安全) な処理になっています。
func (s *postService) List() []model.Post {
	s.mu.Lock()
	defer s.mu.Unlock()

	return append([]model.Post(nil), s.posts...)
}
  • 投稿一覧を返します。

次に、サービス層の実装に合わせてhandler/post_handler.go を修正

(差分は省略)

handler/post_handler.go
package handler

import (
	"go-gin-blog-api/model"
	"go-gin-blog-api/service"
	"net/http"

	"github.com/gin-gonic/gin"
)

type PostHandler struct {
	postService service.PostService
}

func NewPostHandler(s service.PostService) *PostHandler {
	return &PostHandler{postService: s}
}

func (h *PostHandler) RegisterRoutes(r *gin.Engine) {
	r.POST("/posts", h.CreatePost)
	r.GET("/posts", h.GetPosts)
}

func (h *PostHandler) CreatePost(c *gin.Context) {
	var newPost model.Post
	if err := c.ShouldBindJSON(&newPost); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	created := h.postService.Create(newPost)
	c.JSON(http.StatusCreated, created)
}

func (h *PostHandler) GetPosts(c *gin.Context) {
	posts := h.postService.List()
	c.JSON(http.StatusOK, posts)
}

コードの解説

type PostHandler struct {
	postService service.PostService
}
  • ハンドラーは HTTP リクエストを受け取る層です。
  • ここではビジネスロジックを持たず、サービス層に処理を委譲するのがポイント。
func NewPostHandler(s service.PostService) *PostHandler {
	return &PostHandler{postService: s}
}
  • DI(依存性注入)パターンです。
  • テスト時にモックサービスを渡せるようになるので、テストしやすい構成になります。
func (h *PostHandler) CreatePost(c *gin.Context) {
	var newPost model.Post
	if err := c.ShouldBindJSON(&newPost); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	created := h.postService.Create(newPost)
	c.JSON(http.StatusCreated, created)
}
  • JSONを model.Post にマッピング(バインド)。
  • エラーがあれば 400 を返す。
  • サービスに処理を渡して、その結果を JSON で返す。
func (h *PostHandler) GetPosts(c *gin.Context) {
	posts := h.postService.List()
	c.JSON(http.StatusOK, posts)
}
  • 投稿一覧を取得して、そのまま返します。

main.go を修正

main.go
package main

import (
	"go-gin-blog-api/handler"
+	"go-gin-blog-api/service"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/health", func(c *gin.Context) {
		c.JSON(200, gin.H{"status": "ok"})
	})

- handler.RegisterPostRoutes(r)
+	postService := service.NewPostService()
+	postHandler := handler.NewPostHandler(postService)
+	postHandler.RegisterRoutes(r)
	r.Run(":8080")
}

コードの書き換えが終わったら動作確認を行います。

ブログの投稿とブログ一覧の取得が正常に行えるか確認します。

リポジトリ層の設計と実装

サービス層までできたら、次に進めたいのが「リポジトリ層」の実装です。

リポジトリ層は、永続化の責務を持つレイヤーで、データベースやファイル、メモリなどストレージの扱いを抽象化します。これにより、アプリケーションのビジネスロジック(サービス層)は、どこに保存するかを意識せずにデータを扱えるようになります。

ディレクトリ構成の再確認

go-gin-blog-api/
├── model/
│   └── post.go
├── repository/
│   └── post_repository.go   👈 ここ!
├── service/
│   └── post_service.go
├── handler/
│   └── post_handler.go
├── main.go
repository/post_repository.go
package repository

import (
	"go-gin-blog-api/model"
	"sync"
)

type PostRepository interface {
	Save(post model.Post) model.Post
	FindAll() []model.Post
}

type postRepository struct {
	mu    sync.Mutex
	posts []model.Post
}

func NewPostRepository() PostRepository {
	return &postRepository{
		posts: make([]model.Post, 0),
	}
}

func (r *postRepository) Save(post model.Post) model.Post {
	r.mu.Lock()
	defer r.mu.Unlock()

	r.posts = append(r.posts, post)
	return post
}

func (r *postRepository) FindAll() []model.Post {
	r.mu.Lock()
	defer r.mu.Unlock()

	return append([]model.Post(nil), r.posts...)
}

コード解説

  • PostRepository interface
    • リポジトリが提供する操作(保存・取得)を定義します。
    • これにより、後からDBや外部APIへの切り替えも簡単になります(インタフェース駆動設計)。
  • postRepository struct
    • 現在はメモリ上に posts []model.Post としてデータを保持しています。
    • 将来的にRDBやNoSQLに置き換える際にはこの構造体の中身だけ差し替えればOKです。
  • 同期処理
    • 複数のリクエストが同時に投稿しても壊れないよう、sync.Mutex による排他制御を行っています。

なぜリポジトリ層が重要なのか?

理由 説明
責務の分離 データ操作の実装を分離し、サービス層をシンプルに保つ
テスト容易性 モック化しやすくなり、サービス層のユニットテストが簡単になる
柔軟な拡張性 DB接続に切り替えたくなっても、影響範囲が小さい

次にリポジトリ層に合わせてサービス層を修正します。

service/post_service.go
package service

import (
	"go-gin-blog-api/model"
	"go-gin-blog-api/repository"
)

type PostService interface {
	Create(post model.Post) model.Post
	List() []model.Post
}

type postService struct {
	repo repository.PostRepository
}

func NewPostService(r repository.PostRepository) PostService {
	return &postService{repo: r}
}

func (s *postService) Create(post model.Post) model.Post {
	return s.repo.Save(post)
}

func (s *postService) List() []model.Post {
	return s.repo.FindAll()
}
  • PostService:サービス層のインターフェース。
  • postService:実装構造体。リポジトリを通してデータを操作します。
  • ここではビジネスロジックは単純ですが、将来的にバリデーションやログ出力などが追加されてもこの層にまとめられます。

最後にmain.goを修正します。

main.go
package main

import (
	"go-gin-blog-api/handler"
+	"go-gin-blog-api/repository"
	"go-gin-blog-api/service"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/health", func(c *gin.Context) {
		c.JSON(200, gin.H{"status": "ok"})
	})

+	postRepo := repository.NewPostRepository()
-	postService := service.NewPostService()
+	postService := service.NewPostService(postRepo)
	postHandler := handler.NewPostHandler(postService)
	postHandler.RegisterRoutes(r)

	r.Run(":8080")
}
  • 依存関係はすべて main.go で注入しています(依存性注入)。
  • これにより各層が疎結合になり、テストや差し替えがしやすくなります。
  • RegisterRouteshandler 内でルーティング定義を一元化できるので、main.go の見通しもスッキリします。

記事の取得・更新・削除機能の追加

ここまでで、記事の一覧取得と新規投稿までを実装しました。
ここからは、ブログAPIとしてもう一歩進めて「記事を取得・更新・削除する機能」を追加していきます。

APIエンドポイント

以下のような3つのRESTfulなAPIを追加します。

メソッド パス 説明
GET /posts/:id 指定したIDの記事を取得
PATCH /posts/:id 記事の部分更新(titleなど)
DELETE /posts/:id 記事の削除

リポジトリ層の拡張

リポジトリ層では、メモリ上のデータに対して以下の操作ができるようにします。

  • IDで1件取得する FindByID
  • 部分的に更新する Update
  • 記事を削除する Delete
repository/post_repository.go
func (r *postRepository) FindByID(id string) (*model.Post, bool) {
	for _, post := range r.posts {
		if post.ID == id {
			return &post, true
		}
	}
	return nil, false
}

func (r *postRepository) Update(id string, updated model.Post) (*model.Post, bool) {
	for i, post := range r.posts {
		if post.ID == id {
			if updated.Title != "" {
				r.posts[i].Title = updated.Title
			}
			if updated.Content != "" {
				r.posts[i].Content = updated.Content
			}
			if updated.Author != "" {
				r.posts[i].Author = updated.Author
			}
			return &r.posts[i], true
		}
	}
	return nil, false
}

func (r *postRepository) Delete(id string) bool {
	for i, post := range r.posts {
		if post.ID == id {
			r.posts = append(r.posts[:i], r.posts[i+1:]...)
			return true
		}
	}
	return false
}

インターフェイスの更新も行います。

repository/post_repository.go
type PostRepository interface {
	Create(post model.Post) model.Post
	FindAll() []model.Post
+	FindByID(id string) (*model.Post, bool)
+	Update(id string, updated model.Post) (*model.Post, bool)
+	Delete(id string) bool
}

サービス層の更新

サービス層では、リポジトリの操作をラップする形で以下のメソッドを追加します。

service/post_service.go
func (s *postService) GetByID(id string) (*model.Post, bool) {
	return s.repo.FindByID(id)
}

func (s *postService) Update(id string, post model.Post) (*model.Post, bool) {
	return s.repo.Update(id, post)
}

func (s *postService) Delete(id string) bool {
	return s.repo.Delete(id)
}

インターフェイスの更新も行います。

service/post_service.go
type PostService interface {
	Create(post model.Post) model.Post
	GetAll() []model.Post
+	GetByID(id string) (*model.Post, bool)
+	Update(id string, updated model.Post) (*model.Post, bool)
+	Delete(id string) bool
}

ハンドラーの実装

そして、ハンドラー層で実際のリクエストに応じた処理を定義していきます。

handler/post_handler.go
func (h *PostHandler) RegisterRoutes(r *gin.Engine) {
	r.POST("/posts", h.CreatePost)
	r.GET("/posts", h.GetPosts)
+	r.GET("/posts/:id", h.GetPostByID)
+	r.PATCH("/posts/:id", h.UpdatePost)
+	r.DELETE("/posts/:id", h.DeletePost)
}

それぞれのエンドポイントに対応したハンドラは以下の通り。

handler/post_handler.go
func (h *PostHandler) GetPostByID(c *gin.Context) {
	id := c.Param("id")
	post, found := h.postService.GetByID(id)
	if !found {
		c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
		return
	}
	c.JSON(http.StatusOK, post)
}

func (h *PostHandler) UpdatePost(c *gin.Context) {
	id := c.Param("id")
	var update model.Post
	if err := c.ShouldBindJSON(&update); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	post, updated := h.postService.Update(id, update)
	if !updated {
		c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
		return
	}
	c.JSON(http.StatusOK, post)
}

func (h *PostHandler) DeletePost(c *gin.Context) {
	id := c.Param("id")
	if deleted := h.postService.Delete(id); !deleted {
		c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
		return
	}
	c.Status(http.StatusNoContent)
}

動作確認:curlで試す

以下のようにcurlコマンドでAPIの挙動を確認できます。

記事を1件取得

curl http://localhost:8080/posts/1

Image from Gyazo

記事を部分更新

curl -X PATCH http://localhost:8080/posts/1 \
  -H "Content-Type: application/json" \
  -d '{"title": "新しいタイトル"}'

Image from Gyazo

記事を削除

curl -X DELETE http://localhost:8080/posts/1

Image from Gyazo

おわりに

ここまでで、Go × Gin を使って、ブログ記事投稿APIをMVC構成で組み立てる基礎的な流れを一通り体験してきました。

  • Ginの最小構成から始まり
  • モデル・サービス・リポジトリといったレイヤー分離
  • ハンドラの整理とルーティングの定義
  • 記事投稿、取得、更新、削除といった基本的なAPI設計

を段階的に実装してきました。

今回の実装はメモリ上でのデータ管理にとどまっており、本格的な運用にはまだ準備が必要です。
実際の開発現場では、永続化されたデータベースへの接続、安定したバリデーションロジック、
そしてなにより堅牢なテストが欠かせません。

次回は、以下の内容に踏み込んでいきます。

  • Go × Gin × MVC構成で実践する堅牢なテスト設計と実装ガイド
    • ユニットテスト、インテグレーションテストの分離と戦略
    • モックを活用したテストの書き方
    • httptesttestify を用いたGinアプリのE2Eに近いテスト

テストと永続化の導入を通して、安心して育てられるAPI設計を一緒に探っていきます。

https://shinagawa-web.com/blogs/go-gin-mvc-testing

Xでシェア
Facebookでシェア
LinkedInでシェア

記事に関するお問い合わせ📝

記事の内容に関するご質問、ご意見などは、下記よりお気軽にお問い合わせください。
ご質問フォームへ

技術支援などお仕事に関するお問い合わせ📄

技術支援やお仕事のご依頼に関するお問い合わせは、下記よりお気軽にお問い合わせください。
お問い合わせフォームへ

関連する技術ブログ

弊社の技術支援サービス

お問い合わせ

経営と現場をつなぐ“共創型”の技術支援。
成果に直結するチーム・技術・プロセスを共に整えます。

お問い合わせ