Building My First Go REST API: From Zero to Production-Ready Backend
Journey into Go development: Building a complete REST API with Fiber, PostgreSQL, and clean architecture patterns. Discovering why Go is dominating backend development.
Building My First Go REST API: From Zero to Production-Ready Backend
After building platforms with Node.js and Python, I decided to dive into Go to understand why it's becoming the go-to choice for backend services. Today I built my first Go REST API, and the experience was eye-opening.
Why Go for Backend Development?
Coming from JavaScript/TypeScript background with BKRS.io (serving 7K+ users), I was curious about Go's reputation for performance and simplicity. The compile speed alone made me understand why companies like Google, Uber, and Docker choose Go for their backend services.
Project Overview: Todo REST API
I built a complete CRUD API for a todo application with:
- Framework: Fiber (Express.js-like for Go)
- Database: PostgreSQL with pgx driver
- Architecture: Dependency injection pattern
- Dev Tools: Air for hot reloading
Step-by-Step Implementation
1. Project Setup & Dependencies
First, I initialized a new Go module and installed the essential packages:
go mod init todo-api
go get github.com/gofiber/fiber/v2
go get github.com/jackc/pgx/v5
go get github.com/joho/godotenv
go get github.com/cosmtrek/air # For hot reloading
Air Configuration (air.toml):
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_root = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
time = false
[misc]
clean_on_exit = false
2. Core Data Structures
type createTodoRequest struct {
Body string `json:"body"`
}
type Todo struct {
ID int `json:"id" db:"id"`
Completed bool `json:"completed" db:"completed"`
Body string `json:"body" db:"body"`
}
type App struct {
DB *pgx.Conn
App *fiber.App
}
3. Database Connection & Setup
func main() {
// Load environment variables
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file", err)
}
// Connect to PostgreSQL
DB := os.Getenv("DATABASE_URL")
conn, err := pgx.Connect(context.Background(), DB)
if err != nil {
panic(err)
}
defer conn.Close(context.Background())
// Create table if not exists
_, err = conn.Exec(context.Background(),
`CREATE TABLE IF NOT EXISTS todos(
id SERIAL PRIMARY KEY,
completed BOOLEAN NOT NULL DEFAULT FALSE,
body TEXT NOT NULL
);`)
if err != nil {
panic(err)
}
}
4. Dependency Injection Pattern
Instead of using global variables, I implemented a clean dependency injection pattern using structs:
type App struct {
DB *pgx.Conn
App *fiber.App
}
// All handlers are methods on the App struct
func (a *App) getTodos(c *fiber.Ctx) error { /* ... */ }
func (a *App) createTodo(c *fiber.Ctx) error { /* ... */ }
func (a *App) updateTodo(c *fiber.Ctx) error { /* ... */ }
func (a *App) deleteTodo(c *fiber.Ctx) error { /* ... */ }
This approach provides:
- Testability: Easy to mock dependencies
- Clean Architecture: Clear separation of concerns
- Maintainability: Explicit dependencies
5. REST API Endpoints
// Setup routes
a.App.Get("/api/todos", a.getTodos)
a.App.Post("/api/todos", a.createTodo)
a.App.Put("/api/todos/:id", a.updateTodo)
a.App.Delete("/api/todos/:id", a.deleteTodo)
6. CRUD Operations Implementation
GET - Fetch All Todos:
func (a *App) getTodos(c *fiber.Ctx) error {
rows, err := a.DB.Query(context.Background(), "SELECT * FROM todos")
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
defer rows.Close()
todos := []Todo{}
for rows.Next() {
var t Todo
if err := rows.Scan(&t.ID, &t.Completed, &t.Body); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
todos = append(todos, t)
}
return c.Status(200).JSON(todos)
}
POST - Create New Todo:
func (a *App) createTodo(c *fiber.Ctx) error {
req := new(createTodoRequest)
// Parse JSON body
if err := c.BodyParser(req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
}
// Validation
if req.Body == "" {
return c.Status(400).JSON(fiber.Map{"error": "Todo body cannot be empty"})
}
todo := &Todo{Body: req.Body}
// Safe parameterized query
query := `INSERT INTO todos (body, completed) VALUES ($1, $2) RETURNING id`
err := a.DB.QueryRow(context.Background(), query, todo.Body, false).Scan(&todo.ID)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(todo)
}
Key Learnings & Insights
1. Compilation Speed is Game-Changing
Coming from Node.js development, Go's near-instantaneous compilation was remarkable. No more waiting for webpack builds or dealing with complex build pipelines.
2. Type Safety Without Complexity
Go's type system feels more approachable than TypeScript while providing similar safety benefits. The explicit error handling, while verbose, makes debugging much clearer.
3. Performance Out of the Box
Even this simple API felt snappy. The binary deployment model eliminates dependency management headaches I've experienced with Node.js projects.
4. Clean Architecture Patterns
The dependency injection pattern using structs feels natural and promotes testable code. It's similar to what I implemented in my BKRS.io platform but more explicit.
Code Improvements & Best Practices
1. Error Handling Enhancement
// Instead of panic, use proper error handling
if err != nil {
log.Printf("Database connection failed: %v", err)
return fmt.Errorf("failed to connect to database: %w", err)
}
2. Environment Validation
func validateEnv() error {
required := []string{"DATABASE_URL", "PORT"}
for _, env := range required {
if os.Getenv(env) == "" {
return fmt.Errorf("required environment variable %s is not set", env)
}
}
return nil
}
3. Graceful Shutdown
// Add signal handling for graceful shutdown
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
log.Println("Gracefully shutting down...")
a.App.Shutdown()
}()
Performance Comparison
Having built similar APIs in Node.js for BKRS.io, I noticed:
- Startup Time: ~50ms vs ~2s for Node.js
- Memory Usage: ~10MB vs ~50MB for equivalent Node.js API
- Response Time: Consistently under 50ms for simple queries
Is This Clean Code?
The dependency injection pattern I implemented definitely moves toward clean architecture:
✅ Single Responsibility: Each handler has one job
✅ Dependency Inversion: Database is injected, not hardcoded
✅ Testability: Easy to mock the database connection
✅ Explicit Dependencies: Clear what each function needs
However, for production, I'd add:
- Interface abstractions for the database
- Proper logging middleware
- Request validation middleware
- Error handling middleware
Next Steps
This first Go project convinced me to explore further:
- gRPC Integration: For high-performance service communication
- Testing Strategies: Go's built-in testing tools
- Deployment: Docker containerization and Kubernetes
Conclusion
Building my first Go REST API was surprisingly smooth. The language's simplicity, combined with excellent tooling like Fiber and pgx, made the development experience enjoyable.
The compilation speed alone makes me understand why companies choose Go for backend services. While the syntax took some adjustment coming from JavaScript, the explicit nature of Go actually made the code more readable and maintainable.
Would I use Go for my next backend project? Absolutely. The performance benefits and deployment simplicity make it an excellent choice, especially for APIs that need to handle high concurrency.
Complete Code: The full implementation is available as a reference for anyone starting their Go journey. Feel free to reach out if you have questions about Go development or want to discuss backend architecture patterns!
Tech Stack Used:
- Go 1.25+
- Fiber v2 (Web Framework)
- PostgreSQL (Database)
- pgx v5 (PostgreSQL Driver)
- Air (Hot Reloading)
- godotenv (Environment Management)