Skip to main content

Overview

This chapter demonstrates building a fully functional RESTful API for managing a movie collection. You’ll learn how to structure REST endpoints, handle JSON data, work with HTTP methods, and use the Gorilla Mux router for dynamic URL parameters.
This project is part of Chapter 9: Web Basics and serves as the foundation for understanding HTTP servers in Go.

What You’ll Build

A complete CRUD (Create, Read, Update, Delete) API that manages movies with directors. The API uses in-memory storage and demonstrates RESTful principles.

Key Concepts

1. JSON Encoding in Go

Go’s encoding/json package provides powerful serialization capabilities. For a struct to be JSON-encodable, fields must be exported (capitalized).
type Movie struct {
    ID       string    `json:"id"`
    Isbn     string    `json:"isbn"`
    Title    string    `json:"title"`
    Director *Director `json:"director"`
}

type Director struct {
    Firstname string `json:"firstname"`
    Lastname  string `json:"lastname"`
}
Common Pitfall: If struct fields are lowercase (unexported), they won’t be visible to the JSON encoder. Always capitalize field names and use struct tags for JSON keys.

2. Gorilla Mux Router

While Go’s standard library provides http.ServeMux, Gorilla Mux offers advanced routing features like URL parameters, method-based routing, and regex patterns.
import "github.com/gorilla/mux"

r := mux.NewRouter()
r.HandleFunc("/movies/{id}", getMovie).Methods("GET")

3. REST Endpoint Design

The API follows REST conventions where:
  • Resource collections use plural nouns (/movies)
  • Individual resources include identifiers (/movies/{id})
  • HTTP methods indicate the operation (GET, POST, PUT, DELETE)

REST Endpoints

Here’s the complete API specification:
MethodEndpointDescriptionRequest BodyResponse
GET/moviesGet all moviesNoneArray of movies
GET/movies/{id}Get a specific movie by IDNoneSingle movie object
POST/moviesCreate a new movieMovie JSONCreated movie
PUT/movies/{id}Update an existing movieMovie JSONUpdated movie
DELETE/movies/{id}Delete a movieNoneEmpty response

Implementation

Step 1: Define Data Models

Start by defining the data structures that represent your domain:
main.go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    
    "github.com/gorilla/mux"
)

type Movie struct {
    ID       string    `json:"id"`
    Isbn     string    `json:"isbn"`
    Title    string    `json:"title"`
    Director *Director `json:"director"`
}

type Director struct {
    Firstname string `json:"firstname"`
    Lastname  string `json:"lastname"`
}

// In-memory storage
var movies []Movie

Step 2: Implement GET Operations

func getMovies(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(movies)
}
This handler:
  1. Sets the Content-Type header to indicate JSON response
  2. Encodes the entire movies slice directly to the response writer

Step 3: Implement POST (Create)

func createMovies(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    
    var movie Movie
    json.NewDecoder(r.Body).Decode(&movie)
    
    movies = append(movies, movie)
    json.NewEncoder(w).Encode(movie)
}
1

Decode Request Body

json.NewDecoder(r.Body).Decode(&movie) reads the request body and unmarshals JSON into the movie struct.
2

Store the Movie

Append the new movie to the in-memory slice.
3

Return Created Resource

Encode and return the created movie to confirm the operation.

Step 4: Implement PUT (Update)

func updateMovies(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    params := mux.Vars(r)
    
    for index, movie := range movies {
        if movie.ID == params["id"] {
            // Remove old movie
            movies = append(movies[:index], movies[index+1:]...)
            
            // Decode and add new movie
            var movie Movie
            json.NewDecoder(r.Body).Decode(&movie)
            movies = append(movies, movie)
            
            json.NewEncoder(w).Encode(movie)
            return
        }
    }
}
This implementation uses a replace strategy: it removes the old entry and adds the new one. In production, you’d typically update fields in place.

Step 5: Implement DELETE

func deleteMovies(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    params := mux.Vars(r)
    
    for index, movie := range movies {
        if movie.ID == params["id"] {
            movies = append(movies[:index], movies[index+1:]...)
            break
        }
    }
}
The slice manipulation movies[:index] and movies[index+1:] creates a new slice excluding the deleted element.

Step 6: Wire Up Routes and Start Server

func main() {
    r := mux.NewRouter()
    
    // Seed data
    movies = append(movies, Movie{
        ID:    "1",
        Isbn:  "438227",
        Title: "Movie One",
        Director: &Director{
            Firstname: "John",
            Lastname:  "Doe",
        },
    })
    movies = append(movies, Movie{
        ID:    "2",
        Isbn:  "454555",
        Title: "Movie Two",
        Director: &Director{
            Firstname: "Steve",
            Lastname:  "Smith",
        },
    })
    
    // Register routes
    r.HandleFunc("/movies", getMovies).Methods("GET")
    r.HandleFunc("/movies/{id}", getMovie).Methods("GET")
    r.HandleFunc("/movies", createMovies).Methods("POST")
    r.HandleFunc("/movies/{id}", updateMovies).Methods("PUT")
    r.HandleFunc("/movies/{id}", deleteMovies).Methods("DELETE")
    
    fmt.Printf("Starting server at port 8000\n")
    log.Fatal(http.ListenAndServe(":8000", r))
}

Testing the API

Using cURL

curl http://localhost:8000/movies

Running the Project

1

Install Dependencies

cd CRUD
go mod init your-module-name
go get github.com/gorilla/mux
2

Run the Server

go run main.go
You should see: Starting server at port 8000
3

Test Endpoints

Open another terminal and test the endpoints using cURL or a tool like Postman.

Important Concepts

Content-Type Header

Always set Content-Type to inform clients about the response format:
w.Header().Set("Content-Type", "application/json")

Request Body Decoding

Go provides a streaming decoder that reads directly from the request:
var movie Movie
json.NewDecoder(r.Body).Decode(&movie)
This is more efficient than reading the entire body into memory first.

URL Parameters

Gorilla Mux makes extracting URL parameters simple:
params := mux.Vars(r)  // Returns map[string]string
id := params["id"]

Method Filtering

Restrict handlers to specific HTTP methods:
r.HandleFunc("/movies", getMovies).Methods("GET")
r.HandleFunc("/movies", createMovies).Methods("POST")

Best Practices

Production Considerations: This example uses in-memory storage which resets on server restart. Real applications should use a database.
  1. Error Handling: Add proper error handling for JSON decode operations
  2. Validation: Validate incoming data before processing
  3. Status Codes: Return appropriate HTTP status codes (201 for created, 404 for not found)
  4. ID Generation: Use UUIDs or database auto-increment instead of client-provided IDs
  5. Middleware: Add logging, authentication, and CORS middleware

Next Steps

HTML Forms

Learn how to handle HTML form submissions

Authentication

Add JWT authentication to your APIs

Summary

You’ve learned:
  • How to structure a RESTful API in Go
  • Working with JSON encoding/decoding
  • Using Gorilla Mux for advanced routing
  • Implementing CRUD operations
  • Handling HTTP methods and URL parameters
This foundation prepares you for more advanced web development topics including authentication, middleware, and database integration.