Build Your First REST API with Go: A Complete Guide for Beginners

Are you ready to build your first production-ready REST API with Go? In this hands-on tutorial, we’ll create a task management API from scratch, learning essential Go programming concepts along the way. Whether you’re new to Go or looking to enhance your backend development skills, this guide will walk you through every step of building a robust API.

This is Part 2 of our “Mastering Go Programming” series. If you haven’t already, check out Part 1: Getting Started with Go in 2024 first.

What You’ll Learn in This Tutorial

By following this tutorial, you’ll learn how to:

Prerequisites:

Creating a Production-Ready Project Structure

First, let’s set up our project with a scalable directory structure that follows Go best practices:

mkdir task-manager
cd task-manager
go mod init github.com/yourusername/task-manager

Create this directory structure for optimal organization:

task-manager/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── handlers/
│   │   └── task_handlers.go
│   └── models/
│       └── task.go
├── pkg/
│   └── middleware/
│       └── logging.go
└── go.mod

Setting Up Our Task Model

Let’s create our task model with proper JSON tags and validation. Create internal/models/task.go:

package models

import (
    "time"
    "errors"
)

type Task struct {
    ID          string    `json:"id"`
    Title       string    `json:"title"`
    Description string    `json:"description"`
    Status      string    `json:"status"`
    DueDate     time.Time `json:"due_date"`
    CreatedAt   time.Time `json:"created_at"`
    UpdatedAt   time.Time `json:"updated_at"`
}

// TaskStatus represents the valid states of a task
type TaskStatus string

const (
    StatusPending   TaskStatus = "pending"
    StatusInProgress TaskStatus = "in_progress"
    StatusCompleted  TaskStatus = "completed"
)

// Validate checks if the task data is valid
func (t *Task) Validate() error {
    if t.Title == "" {
        return errors.New("title is required")
    }
    if len(t.Title) < 3 {
        return errors.New("title must be at least 3 characters long")
    }
    return nil
}

// Store tasks in memory (we'll replace this with a database in Part 3)
var Tasks = make(map[string]Task)

Implementing RESTful Handlers

Create internal/handlers/task_handlers.go to handle our API endpoints:

package handlers

import (
    "encoding/json"
    "net/http"
    "time"
    "github.com/google/uuid"
    "../models"
)

// CreateTaskHandler handles POST requests to create new tasks
func CreateTaskHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var task models.Task
    if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }

    // Validate task data
    if err := task.Validate(); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // Generate UUID and set timestamps
    task.ID = uuid.New().String()
    task.CreatedAt = time.Now()
    task.UpdatedAt = time.Now()
    task.Status = string(models.StatusPending)

    // Store the task
    models.Tasks[task.ID] = task

    // Return the created task
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(task)
}

// GetTasksHandler returns all tasks
func GetTasksHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    tasks := make([]models.Task, 0, len(models.Tasks))
    for _, task := range models.Tasks {
        tasks = append(tasks, task)
    }

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

Setting Up the Main Application

Create cmd/api/main.go to tie everything together:

package main

import (
    "log"
    "net/http"
    "github.com/yourusername/task-manager/internal/handlers"
    "github.com/yourusername/task-manager/pkg/middleware"
)

func main() {
    // Initialize router
    mux := http.NewServeMux()

    // Register routes with logging middleware
    mux.HandleFunc("/api/tasks", middleware.LogRequest(handlers.GetTasksHandler))
    mux.HandleFunc("/api/task", middleware.LogRequest(handlers.CreateTaskHandler))

    // Configure server
    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // Start server
    log.Printf("Server starting on http://localhost:8080")
    if err := server.ListenAndServe(); err != nil {
        log.Fatal("Server failed to start:", err)
    }
}

Testing Your API

Let’s test our API endpoints using cURL:

# Create a new task
curl -X POST http://localhost:8080/api/task \
-H "Content-Type: application/json" \
-d '{
    "title": "Learn Go Testing",
    "description": "Learn how to write unit tests in Go",
    "due_date": "2024-02-01T15:04:05Z"
}'

# Get all tasks
curl http://localhost:8080/api/tasks

Best Practices and Tips

  1. Error Handling

    • Always return appropriate HTTP status codes
    • Provide meaningful error messages
    • Log errors for debugging
    • Never expose sensitive information in error responses
  2. API Design

    • Use plural nouns for resource endpoints
    • Keep URLs consistent and intuitive
    • Version your API (/api/v1/tasks)
    • Use appropriate HTTP methods
  3. Code Organization

    • Separate concerns (handlers, models, middleware)
    • Use interfaces for flexibility
    • Keep functions small and focused
    • Comment complex logic

Next Steps: Extending Your API

Try these challenges to enhance your API:

  1. Add endpoints for:

    • Updating tasks (PUT /api/task/{id})
    • Deleting tasks (DELETE /api/task/{id})
    • Getting a single task (GET /api/task/{id})
  2. Implement advanced features:

    • Task filtering and sorting
    • Pagination
    • Task categories or tags
    • Due date notifications

Coming Up in Part 3

In the next tutorial, we’ll enhance our API with:

Share Your Progress!

How did you customize your task manager API? Share your implementations and questions in the comments below. Don’t forget to follow our blog for weekly Go tutorials and tips!

Additional Resources

Last updated: January 5, 2025

Advertisement