- Go Monk
- Posts
- Todo REST API with a database
Todo REST API with a database
Adding persistence
This post builds on the previous one, where we created a simple REST API to manage an in-memory todo list. Keeping data in memory is fine for quick demos or tests, but real-world applications need to persist data so it survives server restarts. In this post, we’ll walk through adding SQLite-based persistence to our todo API.
The Problem: In-Memory Storage
Our original implementation used an in-memory store like this:
type TaskStoreInMemory struct {
sync.Mutex
tasks map[int]Task
nextId int
}
This approach was fast and simple, but every time the server restarted, all tasks were lost. We need a way to store tasks on disk.
Since we now anticipate having multiple implementations of the task store, let’s define an interface:
type TaskStore interface {
CreateTask(text string) (int, error)
GetTasks() []Task
GetTask(id int) (Task, error)
DeleteTask(id int) error
}
The in-memory task store already almost satisfies this interface. We only need to modify its CreateTask
method that now must return (int, error)
instead of just int
, allowing the handler to respond appropriately to database errors. We keep the in-memory implementation in taskstore_mem.go
.
Now, let's look at something more persistent.
The Solution: SQLite
SQLite is a lightweight, file-based database that’s perfect for small projects and prototyping. We implement a new TaskStore
using SQLite, leveraging Go’s database/sql
package and the popular github.com/mattn/go-sqlite3
driver.
We create a new file, taskstore_sqlite.go
and implement the TaskStore interface. Each method (CreateTask, GetTasks, GetTask, DeleteTask) is implemented using SQL queries to interact with the database.
Switching Between In-Memory and SQLite Stores
We update our server to allow switching between the in-memory and SQLite-backed stores using a command-line flag. Here’s the new main.go
logic:
func main() {
persist := flag.Bool("persist", false, "persist tasks to SQLite")
dbpath := flag.String("dbpath", "tasks.db", "path to SQLite file")
flag.Parse()
var store todo.TaskStore
var err error
if *persist {
store, err = todo.NewSqliteTaskStore(*dbpath)
} else {
store, err = todo.NewInMemoryTaskStore()
}
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
handler := handler.NewTaskHandler(store)
mux.HandleFunc("POST /task", handler.AddTask)
mux.HandleFunc("GET /tasks", handler.GetTasks)
mux.HandleFunc("GET /task/{id}", handler.GetTask)
mux.HandleFunc("DELETE /task/{id}", handler.DeleteTask)
log.Fatal(http.ListenAndServe(":8080", mux))
}
Now you can run the server with the -persist
flag to use SQLite, or without it to use the in-memory store.
Summary
Adding persistence to your API is a crucial step toward production readiness. With just a few changes, we upgraded our todo API from a toy example to a more robust service. SQLite is a great starting point, and the interface-based approach means you can later swap in more powerful databases with minimal changes. The new command-line flag makes it easy to switch between storage backends, and improved error handling ensures your API responds correctly to failures.