ソースコード
1. プロジェクト構造
restaurant-api/
├── main.go
├── models/
│ ├── user.go
│ └── restaurant.go
├── handlers/
│ ├── user_handler.go
│ └── restaurant_handler.go
├── middleware/
│ └── auth_middleware.go
└── database/
└── database.go
2. データベース接続の設定
database/database.go
package database
import (
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
var DB *gorm.DB
func ConnectDatabase() {
database, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
DB = database
}
3. モデルの定義
models/user.goとmodels/restaurant.goファイルを作成し、モデルを定義します
models/user.go
package models
import "gorm.io/gorm"
type User struct {
gorm.Model
Username string `gorm:"unique" json:"username"`
Password string `json:"password"`
IsSuperUser bool `json:"is_super_user"`
}
models/restaurant.go
package models
import "gorm.io/gorm"
type Restaurant struct {
gorm.Model
Name string `json:"name"`
Description string `json:"description"`
Address string `json:"address"`
UserID uint `json:"user_id"`
Likes int `json:"likes"`
Bookmarks int `json:"bookmarks"`
}
4. 認証ミドルウェアの実装
middleware/auth_middleware.go
package middleware
import (
"net/http"
"strings"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
)
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
c.Abort()
return
}
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"})
c.Abort()
return
}
token, err := jwt.Parse(parts[1], func(token *jwt.Token) (interface{}, error) {
return []byte("your_secret_key"), nil
})
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
c.Abort()
return
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
userID := uint(claims["user_id"].(float64))
isSuperUser := claims["is_super_user"].(bool)
c.Set("user_id", userID)
c.Set("is_super_user", isSuperUser)
c.Next()
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
c.Abort()
return
}
}
}
5. ハンドラーの実装
handlers/user_handler.go
package handlers
import (
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/yourusername/restaurant-api/database"
"github.com/yourusername/restaurant-api/models"
"golang.org/x/crypto/bcrypt"
)
func Register(c *gin.Context) {
var user models.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
}
user.Password = string(hashedPassword)
if err := database.DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
return
}
c.JSON(http.StatusCreated, gin.H{"message": "User created successfully"})
}
func Login(c *gin.Context) {
var loginUser models.User
if err := c.ShouldBindJSON(&loginUser); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := database.DB.Where("username = ?", loginUser.Username).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginUser.Password)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.ID,
"is_super_user": user.IsSuperUser,
"exp": time.Now().Add(time.Hour * 24).Unix(),
})
tokenString, err := token.SignedString([]byte("your_secret_key"))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
return
}
c.JSON(http.StatusOK, gin.H{"token": tokenString})
}
handlers/restaurant_handler.go
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/yourusername/restaurant-api/database"
"github.com/yourusername/restaurant-api/models"
)
func CreateRestaurant(c *gin.Context) {
var restaurant models.Restaurant
if err := c.ShouldBindJSON(&restaurant); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userID, _ := c.Get("user_id")
restaurant.UserID = userID.(uint)
if err := database.DB.Create(&restaurant).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create restaurant"})
return
}
c.JSON(http.StatusCreated, restaurant)
}
func GetRestaurants(c *gin.Context) {
var restaurants []models.Restaurant
if err := database.DB.Find(&restaurants).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch restaurants"})
return
}
c.JSON(http.StatusOK, restaurants)
}
func GetRestaurant(c *gin.Context) {
id := c.Param("id")
var restaurant models.Restaurant
if err := database.DB.First(&restaurant, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Restaurant not found"})
return
}
c.JSON(http.StatusOK, restaurant)
}
func UpdateRestaurant(c *gin.Context) {
id := c.Param("id")
var restaurant models.Restaurant
if err := database.DB.First(&restaurant, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Restaurant not found"})
return
}
userID, _ := c.Get("user_id")
isSuperUser, _ := c.Get("is_super_user")
if restaurant.UserID != userID.(uint) && !isSuperUser.(bool) {
c.JSON(http.StatusForbidden, gin.H{"error": "You don't have permission to update this restaurant"})
return
}
if err := c.ShouldBindJSON(&restaurant); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
database.DB.Save(&restaurant)
c.JSON(http.StatusOK, restaurant)
}
func DeleteRestaurant(c *gin.Context) {
id := c.Param("id")
var restaurant models.Restaurant
if err := database.DB.First(&restaurant, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Restaurant not found"})
return
}
userID, _ := c.Get("user_id")
isSuperUser, _ := c.Get("is_super_user")
if restaurant.UserID != userID.(uint) && !isSuperUser.(bool) {
c.JSON(http.StatusForbidden, gin.H{"error": "You don't have permission to delete this restaurant"})
return
}
database.DB.Delete(&restaurant)
c.JSON(http.StatusOK, gin.H{"message": "Restaurant deleted successfully"})
}
func LikeRestaurant(c *gin.Context) {
id := c.Param("id")
var restaurant models.Restaurant
if err := database.DB.First(&restaurant, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Restaurant not found"})
return
}
restaurant.Likes++
database.DB.Save(&restaurant)
c.JSON(http.StatusOK, gin.H{"message": "Restaurant liked successfully", "likes": restaurant.Likes})
}
func BookmarkRestaurant(c *gin.Context) {
id := c.Param("id")
var restaurant models.Restaurant
if err := database.DB.First(&restaurant, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Restaurant not found"})
return
}
restaurant.Bookmarks++
database.DB.Save(&restaurant)
c.JSON(http.StatusOK, gin.H{"message": "Restaurant bookmarked successfully", "bookmarks": restaurant.Bookmarks})
}
6. main.goの実装
main.goファイルを作成し、ルーティングとサーバーの起動を設定します
main.go
package main
import (
"github.com/gin-gonic/gin"
"github.com/yourusername/restaurant-api/database"
"github.com/yourusername/restaurant-api/handlers"
"github.com/yourusername/restaurant-api/middleware"
"github.com/yourusername/restaurant-api/models"
)
func main() {
r := gin.Default()
database.ConnectDatabase()
database.DB.AutoMigrate(&models.User{}, &models.Restaurant{})
r.POST("/register", handlers.Register)
r.POST("/login", handlers.Login)
authorized := r.Group("/")
authorized.Use(middleware.AuthMiddleware())
{
authorized.POST("/restaurants", handlers.CreateRestaurant)
authorized.GET("/restaurants", handlers.GetRestaurants)
authorized.GET("/restaurants/:id", handlers.GetRestaurant)
authorized.PUT("/restaurants/:id", handlers.UpdateRestaurant)
authorized.DELETE("/restaurants/:id", handlers.DeleteRestaurant)
authorized.POST("/restaurants/:id/like", handlers.LikeRestaurant)
authorized.POST("/restaurants/:id/bookmark", handlers.BookmarkRestaurant)
}
r.Run(":8080")
}
実装のポイント
- 今回の実装は、基本的なRESTful APIの構造を示していますが、実際のプロダクションレベルのアプリケーションでは、ログ機能、より詳細なエラーハンドリング、レート制限、キャッシュなどの追加機能が必要になる場合があります
- また、セキュリティ面でも、HTTPS対応、CORS設定、入力バリデーションなどを考慮する必要があります
1. RestAPI
- RESTful APIは、HTTPメソッド(GET, POST, PUT, DELETE)を使用してリソースに対する操作を定義します
- 各エンドポイントは特定のリソース(この場合はユーザーやレストラン)に対応します
- ステータスコードを使用して、リクエストの結果を示します(例:200 OK, 201 Created, 400 Bad Request, 404 Not Found)
2. Golangの特徴的な機能
- 構造体(struct)を使用してデータモデルを定義します
- インターフェースを使用して、異なる型に共通の振る舞いを定義します(例:gin.HandlerFunc)
- ゴルーチンとチャネルを使用して並行処理を実装できます(今回は明示的に使用していませんが、Ginフレームワークが内部で使用しています)
3. 認証とセキュリティ
- JWTトークンを使用してユーザー認証を実装しています
- パスワードはbcryptを使用してハッシュ化されています
- ミドルウェアを使用して、保護されたルートへのアクセスを制御しています
4. データベースの操作
- GORMを使用してORMを実装し、データベース操作を簡素化しています
- マイグレーション機能を使用して、データベーススキーマを自動的に作成・更新します
5. エラーハンドリング:
- 適切なHTTPステータスコードとエラーメッセージを返すことで、クライアントにエラーの詳細を伝えています
6. 機能の実装
- いいね機能とブックマーク機能は、対応するカウンターをインクリメントすることで実装しています
- これらの機能は、LikeRestaurantとBookmarkRestaurantハンドラーで処理されます
7. ユーザー権限の管理
- 通常ユーザーとスーパーユーザーの2種類のユーザータイプを実装しています
- ユーザーは自分が作成したレストラン情報のみを編集・削除できます
- スーパーユーザーは全てのレストラン情報を編集・削除できます
- これらの権限チェックは、UpdateRestaurantとDeleteRestaurantハンドラーで実装されています
8. APIのバージョニング
- 今回は、明示的にバージョニングを実装していませんが、実際のプロダクションAPIでは、URLパスに/v1/のようなバージョン番号を含めることが一般的です
9. コード構造とモジュール化
- コードを複数のパッケージ(models, handlers, middleware, database)に分割することで、関心の分離を実現し、コードの可読性と保守性を向上させています
10. 設定管理
- 今回は、簡略化のために直接コードに設定値を埋め込んでいますが、実際のアプリケーションでは環境変数や設定ファイルを使用して外部から設定を管理することが推奨されます
11. テスト
- 今回は、テストコードは含まれていませんが、実際の開発では各ハンドラーやミドルウェアに対するユニットテストを書くことが重要です
- Goの標準ライブラリのtestingパッケージを使用してテストを実装できます