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!