Building a REST API in Go: A Comprehensive Guide Using Go Idioms

Building a REST API in Go: A Comprehensive Guide Using Go Idioms

In this post, we'll walk through the process of building a REST API in Go, leveraging Go idioms and best practices. By the end of this guide, you'll have a solid understanding of how to structure your Go project, manage dependencies, and implement a clean and maintainable codebase.

Project Structure

A well-organized project structure is crucial for maintainability and scalability. Here's a typical structure for a Go REST API:


myapp/
├── cmd/
│   └── api/
│       └── main.go
├── pkg/
│   ├── db/
│   │   └── db.go
│   ├── handlers/
│   │   ├── user_handler.go
│   ├── middleware/
│   │   └── auth.go
│   ├── model/
│   │   ├── user.go
│   ├── repository/
│   │   ├── user_repository.go
│   └── route/
│       ├── routes.go
│       └── router.go
└── go.mod        

Setting Up the Project

First, initialize your Go module:

go mod init myapp        

Database Connection

Create adb.gofile to manage the database connection:

// pkg/db/db.go
package db

import (
    "context"
    "errors"
    "fmt"
    "log"
    "os"
    "sync"
    "time"

    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

var (
    instance       *mongo.Database
    once           sync.Once
    initErr        error
    devEnvironment = os.Getenv("DEV_ENVIRONMENT")
)

func Connect() (*mongo.Database, error) {
    once.Do(func() {
        instance, initErr = connect()
    })
    return instance, initErr
}

func connect() (*mongo.Database, error) {
    dbName := os.Getenv("DATABASE_NAME")
    dbUsername := os.Getenv("DATABASE_USERNAME")
    dbHost := os.Getenv("DATABASE_HOST")
    dbPort := os.Getenv("DATABASE_PORT")
    dbPassword := os.Getenv("DATABASE_PASSWORD")

    if dbName == "" || dbUsername == "" || dbHost == "" || dbPort == "" || dbPassword == "" {
        return nil, errors.New("one or more environment variables are not set")
    }

    connectionString := fmt.Sprintf("mongodb://%s:%s@%s:%s/%s?authSource=admin", dbUsername, dbPassword, dbHost, dbPort, dbName)
    clientOpts := options.Client().ApplyURI(connectionString)
    client, err := mongo.Connect(context.TODO(), clientOpts)
    if err != nil {
        return nil, err
    }

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    err = client.Ping(ctx, nil)
    if err != nil {
        return nil, err
    }

    log.Printf("Connected to Database in %s environment\n", devEnvironment)
    return client.Database(dbName), nil
}

func Disconnect(database *mongo.Database) {
    if err := database.Client().Disconnect(context.Background()); err != nil {
        log.Fatalf("Error disconnecting from the database: %v", err)
    }
}        

Models

Define your models in themodelpackage:

// pkg/model/user.go
package model

import "go.mongodb.org/mongo-driver/bson/primitive"

type User struct {
    ID       primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
    Username string             `bson:"username" json:"username"`
    Email    string             `bson:"email" json:"email"`
}

// pkg/model/note.go
package model

import (
    "time"

    "go.mongodb.org/mongo-driver/bson/primitive"
)

type Note struct {
    UserID    primitive.ObjectID `json:"userId" bson:"userId"`
    UserName  string             `json:"userName" bson:"userName"`
    VideoID   primitive.ObjectID `json:"videoId" bson:"videoId"`
    Image     string             `json:"image" bson:"image"`
    Content   string             `json:"content" bson:"content" validate:"max=1000"`
    CreatedAt time.Time          `json:"createdAt" bson:"createdAt"`
    UpdatedAt time.Time          `json:"updatedAt" bson:"updatedAt"`
}        

Repository Pattern

Implement the repository pattern to abstract database operations:

// pkg/repository/user_repository.go
package repository

import (
    "context"

    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "your-project/pkg/model"
)

type UserRepository interface {
    CreateUser(ctx context.Context, user model.User) error
    GetUserByID(ctx context.Context, id primitive.ObjectID) (*model.User, error)
}

type MongoUserRepository struct {
    db *mongo.Database
}

func NewMongoUserRepository(db *mongo.Database) *MongoUserRepository {
    return &MongoUserRepository{db: db}
}

func (r *MongoUserRepository) CreateUser(ctx context.Context, user model.User) error {
    _, err := r.db.Collection("users").InsertOne(ctx, user)
    return err
}

func (r *MongoUserRepository) GetUserByID(ctx context.Context, id primitive.ObjectID) (*model.User, error) {
    var user model.User
    err := r.db.Collection("users").FindOne(ctx, bson.M{"_id": id}).Decode(&user)
    return &user, err
}        

Handlers

Create handlers to manage HTTP requests:

// pkg/handlers/user_handler.go
package handlers

import (
    "context"
    "encoding/json"
    "net/http"

    "github.com/gorilla/mux"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "your-project/pkg/model"
    "your-project/pkg/repository"
)

type UserHandler struct {
    userRepo repository.UserRepository
}

func NewUserHandler(userRepo repository.UserRepository) *UserHandler {
    return &UserHandler{userRepo: userRepo}
}

func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
    var user model.User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid request payload", http.StatusBadRequest)
        return
    }
    defer r.Body.Close()

    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()

    if err := h.userRepo.CreateUser(ctx, user); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusCreated)
}

func (h *UserHandler) GetUserByID(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    userID, err := primitive.ObjectIDFromHex(vars["id"])
    if err != nil {
        http.Error(w, "Invalid user ID", http.StatusBadRequest)
        return
    }

    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()

    user, err := h.userRepo.GetUserByID(ctx, userID)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}        

Middleware

Implement middleware for authentication and other cross-cutting concerns:

// pkg/middleware/auth.go
package middleware

import (
    "net/http"
)

func IsAuthorized(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Implement your authorization logic here
        next.ServeHTTP(w, r)
    })
}        

Routes

Define your routes in theroutepackage:

// pkg/route/routes.go
package route

import (
    "log"
    "net/http"

    "github.com/cosmosinnovate/guide-service/cmd/db"
    "github.com/cosmosinnovate/guide-service/cmd/handlers"
    "github.com/cosmosinnovate/guide-service/cmd/middleware"
    "github.com/cosmosinnovate/guide-service/cmd/repository"
)

type Route struct {
    Name        string
    Method      string
    Pattern     string
    HandlerFunc http.Handler
}

type Routes []Route

func SetupRoutes() Routes {
    database, err := db.Connect()
    if err != nil {
        log.Fatal(err)
    }

    userRepo := repository.NewMongoUserRepository(database)
    userHandler := handlers.NewUserHandler(userRepo)

    return Routes{
        Route{"CreateUser", "POST", "/api/v1/users", http.HandlerFunc(userHandler.CreateUser)},
        Route{"GetUserByID", "GET", "/api/v1/users/{id}", middleware.IsAuthorized(http.HandlerFunc(userHandler.GetUserByID))},
    }
}        

Router

Set up the router and start the server:

// pkg/route/router.go
package route

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/handlers"
    "github.com/gorilla/mux"
)

func NewRouter() {
    routes := SetupRoutes()

    r := mux.NewRouter()
    for _, route := range routes {
        r.
            Methods(route.Method).
            Path(route.Pattern).
            Name(route.Name).
            Handler(route.HandlerFunc)
    }

    headers := handlers.AllowedHeaders([]string{"Content-Type", "Authorization", "application/json; charset=utf-8", "Auth-Type"})
    methods := handlers.AllowedMethods([]string{"GET", "PUT", "PATCH", "POST", "DELETE"})
    origins := handlers.AllowedOrigins([]string{"*"})
    
    fmt.Printf("Server running...\n")
    log.Fatal(http.ListenAndServe(":8080", handlers.CORS(headers, methods, origins)(r)))
}        

Main Application

Finally, create the main application file to start the server:

// cmd/api/main.go
package main

import (
    "github.com/cosmosinnovate/guide-service/cmd/route"
)

func main() {
    route.NewRouter()
}        

Conclusion

By following this guide, you've built a REST API in Go using Go idioms and best practices. This structure ensures your code is clean, maintainable, and scalable. You can now extend this foundation by adding more features, improving error handling, and implementing additional middleware as needed.

Happy coding!

要查看或添加评论,请登录

Taban C.的更多文章

  • AI Is Making Us Dumber? My Shocking Experiment!

    AI Is Making Us Dumber? My Shocking Experiment!

    One of the most interesting things about technology is its immense effect on critical thinking. This has been evident…

    1 条评论
  • Fisker Outsells Volkswagen: A Tale of Resilience in the EV Market

    Fisker Outsells Volkswagen: A Tale of Resilience in the EV Market

    I am a fan of cars especially when I see new companies trying new ways to build cars. I try to root for the under dogs…

    1 条评论
  • Ten essential skills that are hard to learn but pays great success dividends.

    Ten essential skills that are hard to learn but pays great success dividends.

    Things I continue to improve and be good at. :) Speaking up Warren Buffet: Be comfortable doing public speaking.

    1 条评论
  • First Gadget

    First Gadget

    Long time ago, I recall visiting one of our neighbors house in the camps. While at his house, there was a radio laying…

  • Self Innovation

    Self Innovation

    For the first time, I am sharing hacks on “self-innovation” based on actionable items that continue to help me today!…

    1 条评论
  • Could mentorship help with a clear focus?

    Could mentorship help with a clear focus?

    Ten years ago we arrived to Seattle from a refugee camp in Kenya. After 13 years of living in a harsh environment…

    1 条评论
  • Our Hackathon event was featured on the local news (DailyRecords) last year.

    Our Hackathon event was featured on the local news (DailyRecords) last year.

    I would like to share a piece of article on our hackathon event that was written last year by ANDY MATARRESE staff…

  • TRANSITION FROM A REFUGEE TO TECH LIFE

    TRANSITION FROM A REFUGEE TO TECH LIFE

    "No difficulty can discourage, no obstacle dismay, no trouble dishearten the man who has acquired the art of being…

    12 条评论

社区洞察

其他会员也浏览了