DEV Community

Steve Omollo
Steve Omollo

Posted on

Building a CRUD API in Go

Previously, we built a simple task API that could create and retrieve tasks.

If you have been following this series, we've gradually progressed from building a simple HTTP server to creating a JSON-based task API. In this article, we will complete that progression by adding update and delete functionality, giving our API the four operations commonly known as CRUD.

CRUD stands for:

  • Create
  • Read
  • Update
  • Delete

These operations form the foundation of many backend applications and APIs.

In this tutorial, we will extend our task API using only Go's standard library. To keep things simple, we'll continue storing tasks in memory using a slice rather than a database.

By the end, you'll understand:

  • how CRUD APIs work
  • how to handle multiple HTTP methods
  • how to update existing data
  • how to remove data from slices
  • how backend APIs manage resources

Prerequisites

To follow along, you should have:

  • Go installed
  • basic familiarity with Go syntax
  • understanding of the net/http package
  • basic understanding of JSON handling

You can confirm if Go is installed by running:

go version
Enter fullscreen mode Exit fullscreen mode

Step 1 — Create the Project

Create a new folder for the project:

mkdir go-crud-api
cd go-crud-api
Enter fullscreen mode Exit fullscreen mode

Now initialize a Go module:

go mod init go-crud-api
Enter fullscreen mode Exit fullscreen mode

Step 2 — Create the Server File

Create a file called main.go.

Your project structure should now look like this.

go-crud-api/
├─ go.mod
└─ main.go
Enter fullscreen mode Exit fullscreen mode

Step 3 — Write the CRUD API

Open main.go and add the following code:

package main

import (
    "encoding/json"
    "net/http"
)

type Task struct {
    ID        int        `json:"id"`
    Title     string     `json:"title"`
}

var tasks []Task

func tasksHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    switch r.Method {

    case http.MethodGet:
        json.NewEncoder(w).Encode(tasks)

    case http.MethodPost:
        var task Task

        err := json.NewDecoder(r.Body).Decode(&task)
        if err != nil {
            http.Error(w, "Invalid JSON", http.StatusBadRequest)
            return
        }

        tasks = append(tasks, task)
        json.NewEncoder(w).Encode(task)

    case http.MethodPut:
        var updatedTask Task

        err := json.NewDecoder(r.Body).Decode(&updatedTask)
        if err != nil {
            http.Error(w, "Invalid JSON", http.StatusBadRequest)
            return
        }

        for i, task := range tasks {
            if task.ID == updatedTask.ID {
                tasks[i].Title = updatedTask.Title
                json.NewEncoder(w).Encode(tasks[i])
                return
            }
        }

        http.Error(w, "Task not found", http.StatusNotFound)

    case http.MethodDelete:
        var taskToDelete Task

        err := json.NewDecoder(r.Body).Decode(&taskToDelete)
        if err != nil {
            http.Error(w, "Invalid JSON", http.StatusBadRequest)
            return
        }

        for i, task := range tasks {
            if task.ID == taskToDelete.ID {
                tasks = append(tasks[:i], tasks[i+1:]...)
                w.Write([]byte("Task deleted"))
                return
            }
        }

        http.Error(w, "Task not found", http.StatusNotFound)

    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func main() {
    http.HandleFunc("/tasks", tasksHandler)

    http.ListenAndServe(":8080", nil)
}
Enter fullscreen mode Exit fullscreen mode

Now let's unpack what is happening.

Understanding CRUD Operations

CRUD represents the four most common actions performed on data.

Operation HTTP Method
Create POST
Read GET
Update PUT
Delete DELETE

Most backend APIs are built around these operations.

Create and Read

The POST and GET handlers work exactly as they did in the previous tutorial.

  • POST creates tasks
  • GET retrieves tasks

These operations allow us to add and view data.

Understanding Updates

This section handles updates:

case http.MethodPut:
Enter fullscreen mode Exit fullscreen mode

The server expects updated task information in the request body.

For example:

{
    "id": 1,
    "title": "Learn Advanced Go"
}
Enter fullscreen mode Exit fullscreen mode

After decoding the request, we search for a matching task:

for i, task := range tasks {
    if task.ID == updatedTask.ID {
Enter fullscreen mode Exit fullscreen mode

If a match is found, we update the title:

tasks[i].Title = updatedTask.Title
Enter fullscreen mode Exit fullscreen mode

Then we return the updated task as JSON.

Understanding Deletes

This section handles deleting tasks:

case http.MethodDelete:
Enter fullscreen mode Exit fullscreen mode

The client sends the ID of the task to remove:

{
    "id": 1
}
Enter fullscreen mode Exit fullscreen mode

We search for the task with matching ID and remove it from the slice.

The deletion happens here:

tasks = append(tasks[:i], tasks[i+1:]...)
Enter fullscreen mode Exit fullscreen mode

This combines:

  • everything before the task
  • everything after the task

into a new slice that no longer contains the deleted item.

Understanding Slice Deletion

This line often looks strange at first:

tasks = append(tasks[:i], tasks[i+1:]...)
Enter fullscreen mode Exit fullscreen mode

Let's break it down.

Suppose we have:

tasks = [1 2 3 4]
Enter fullscreen mode Exit fullscreen mode

and want to remove:

2
Enter fullscreen mode Exit fullscreen mode

Then:

tasks[:i]
Enter fullscreen mode Exit fullscreen mode

gives:

[1]
Enter fullscreen mode Exit fullscreen mode

and:

tasks[i+1:]
Enter fullscreen mode Exit fullscreen mode

gives:

[3 4]
Enter fullscreen mode Exit fullscreen mode

Appending them together creates:

[1 3 4]
Enter fullscreen mode Exit fullscreen mode

The item has effectively been removed.

Go Tip: The append(tasks[:i], tasks[i+1:]...) pattern is one of the most common ways to remove an element from a slice in Go. You'll see it in many Go projects, so it's worth understanding how it works.

Step 4 — Run the Server

Start the application:

go run main.go
Enter fullscreen mode Exit fullscreen mode

The server should now be running on port 8080.

Step 5 — Create a Task

curl -X POST http://localhost:8080/tasks \
-H "Content-Type: application/json" \
-d '{"id":1,"title":"Learn Go"}'
Enter fullscreen mode Exit fullscreen mode

Response:

{
    "id": 1,
    "title": "Learn Go"
}
Enter fullscreen mode Exit fullscreen mode

Step 6 — Retrieve Tasks

curl http://localhost:8080/tasks
Enter fullscreen mode Exit fullscreen mode

Response:

[
    {
        "id": 1,
        "title": "Learn Go"
    }
]
Enter fullscreen mode Exit fullscreen mode

Step 7 — Update a Task

curl -X PUT http://localhost:8080/tasks \
-H "Content-Type: application/json" \
-d '{"id":1,"title":"Learn Advanced Go"}'
Enter fullscreen mode Exit fullscreen mode

Response:

{
    "id": 1,
    "title": "Learn Advanced Go"
}
Enter fullscreen mode Exit fullscreen mode

Step 8 — Delete a Task

curl -X DELETE http://localhost:8080/tasks \
-H "Content-Type: application/json" \
-d '{"id":1}'
Enter fullscreen mode Exit fullscreen mode

Response:

Task deleted
Enter fullscreen mode Exit fullscreen mode

What Happens When a Request Is Made?

Here's the flow:

  1. The client sends an HTTP request.
  2. Go checks the request method.
  3. The appropriate handler creates, retrieves, updates, or deletes a task.
  4. The server prepares the response.
  5. A response is sent back to the client

This is the foundation of many real-world APIs.

Where to Go Next

Now that we have a simple CRUD API, we could extend it by adding:

  • route parameters
  • persistent databases
  • request validation
  • middleware
  • authentication

This is where backend applications begin moving closer to production-ready systems.

Final Thoughts

We started with a simple task API and extended it into a complete CRUD API.

Along the way, we learned about:

  • HTTP methods
  • updating data
  • deleting data
  • slice manipulation
  • API design

These concepts appear in countless backend applications and services.

Thanks for reading!

Happy coding!

Top comments (0)