REST APIs are everywhere. From mobile apps to web platforms, they connect the front-end and back-end in a clean and standard way. In this post, we’ll write a basic REST API using Go. It will not use any third-party packages. Only the Go standard library. We’ll simulate a database using a simple map. Go is fast, simple, and powerful. It compiles to a single binary. It starts up quickly and uses little memory. Go has strong support for concurrency, and the standard library is great for building APIs.
In a microservices setup, services talk to each other. Often they use REST for this communication.
Project Setup
Create a new directory for your app:
mkdir go-rest-api
cd go-rest-api
Initialize a Go Module. Run this inside the folder this will create a go.mod file.
go mod init go-rest-api
Create a file called main.go
touch main.go
Open main.go and add these imports
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"sync"
)
Define the Person Type
We’ll store and return Person objects through our API. A Person will have three fields: ID, Name, and Email. Let’s define it:
type Person struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
We’re not using a real database in this post. To keep things simple, we’ll use a map to store our data. The keys will be integers (IDs), and the values will be Person structs.
var (
db = make(map[int]Person)
mu sync.Mutex
idCounter = 1
)
Why do we need the Mutex? Because the HTTP server may handle multiple requests at the same time. Without locking, the map can get corrupted. This setup is fine for small apps or demos. For real projects, you should use a proper database like PostgreSQL or MongoDB.
Implementing the Endpoints
Add Person
This endpoint reads a JSON object, gives it an ID, and stores it in our map.
func addPerson(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var p Person
err := json.NewDecoder(r.Body).Decode(&p)
if err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
mu.Lock()
p.ID = idCounter
db[p.ID] = p
idCounter++
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(p)
}
Get One Person
This reads an id from the query string and returns the matching person.
func getPerson(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
idStr := r.URL.Query().Get("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
mu.Lock()
person, exists := db[id]
mu.Unlock()
if !exists {
http.Error(w, "Person not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(person)
}
Get All People
This collects all people from the map and returns them as a JSON array.
func getAllPersons(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
mu.Lock()
persons := make([]Person, 0, len(db))
for _, p := range db {
persons = append(persons, p)
}
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(persons)
}
Update Person
This replaces a person in the map with new data.
func updatePerson(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
idStr := r.URL.Query().Get("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
var updated Person
err = json.NewDecoder(r.Body).Decode(&updated)
if err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
mu.Lock()
_, exists := db[id]
if !exists {
mu.Unlock()
http.Error(w, "Person not found", http.StatusNotFound)
return
}
updated.ID = id
db[id] = updated
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updated)
}
Delete Person
This removes a person from the map by ID.
func deletePerson(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
idStr := r.URL.Query().Get("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
mu.Lock()
_, exists := db[id]
if exists {
delete(db, id)
}
mu.Unlock()
if !exists {
http.Error(w, "Person not found", http.StatusNotFound)
return
}
fmt.Fprintf(w, "Person with ID %d deleted\n", id)
}
Starting the Server
Now we need to connect each function to a URL path. We do this using http.HandleFunc(). Let’s put everything together inside main():
func main() {
http.HandleFunc("/person", addPerson)
http.HandleFunc("/person/get", getPerson)
http.HandleFunc("/persons", getAllPersons)
http.HandleFunc("/person/update", updatePerson)
http.HandleFunc("/person/delete", deletePerson)
fmt.Println("Server running at http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
- This listens on port 8080.
- Each route points to one of our functions.
- We use log.Fatal() so we see any startup errors right away.
Now we’re ready to test the API.
Testing the API
You can test the API using curl, Postman, or any HTTP client. Here are some sample curl commands you can run in your terminal:
Add a New Person
curl -X POST -H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}' \
http://localhost:8080/person
Get a Person by ID
curl http://localhost:8080/person/get?id=1
Get All People
curl http://localhost:8080/persons
Update a Person
curl -X PUT -H "Content-Type: application/json" \
-d '{"name":"Alice Smith","email":"alice.smith@example.com"}' \
"http://localhost:8080/person/update?id=1"
Delete a Person
curl -X DELETE "http://localhost:8080/person/delete?id=1"
Each response will come back as JSON (except for delete, which returns plain text).
Conclusion
We started with nothing, and now we have a working REST API written entirely in Go using only the standard library. It’s a simple project, but it covers a lot of ground. We defined a Person type to model our data, used a map as a basic in-memory database, and created endpoints to add, retrieve, update, and delete records. These are the core operations behind most backend services.
Of course, this isn’t production-ready code and it’s not meant to be. It’s a learning tool. It’s small on purpose, so we can focus on the essentials. Still, even this simple setup gives us a clear view into how Go handles HTTP, JSON, and basic data handling.
Suleyman Cabir Ataman, PhD