Go's standard library is enough to build a real REST API — no framework, no dependencies. This build is a complete CRUD service for "items": net/http handles the routing, an in-memory map guarded by a mutex holds the data, and every change is written to a JSON file so the store survives restarts. Because it's pure standard library, the whole thing compiles to a single binary with nothing to install.
| At a glance | |
|---|---|
| You need | A server running the Go application, and Go on your own computer to build |
| Plan | Any plan — free runs while your session timer has time left, premium runs 24/7 |
| Time | About twenty-five minutes |
| No server yet? | Create your first app server |
🎯 The one Go rule on Falix: the Go application runs a prebuilt binary named
app— nothing compiles on the server. So the real task is getting a Linux binary onto the server. Go on Falix covers the two ways (build locally and upload, or build on Git deploy) in full — this guide gives you the code and points there for the build-and-upload flow.
Step 1 — The files
go.mod — this uses Go 1.22's routing, so declare that version:
module falix-items
go 1.22
main.go — the whole API:
package main
import (
"encoding/json"
"net/http"
"os"
"strconv"
"sync"
)
type Item struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
// Store is an in-memory map guarded by a mutex, persisted to a JSON file.
type Store struct {
mu sync.Mutex
items map[int]Item
nextID int
file string
}
func NewStore(file string) *Store {
s := &Store{items: map[int]Item{}, nextID: 1, file: file}
s.load()
return s
}
func (s *Store) load() {
data, err := os.ReadFile(s.file)
if err != nil {
return // no file yet — start empty
}
var list []Item
if json.Unmarshal(data, &list) == nil {
for _, it := range list {
s.items[it.ID] = it
if it.ID >= s.nextID {
s.nextID = it.ID + 1
}
}
}
}
func (s *Store) save() {
list := make([]Item, 0, len(s.items))
for _, it := range s.items {
list = append(list, it)
}
if data, err := json.MarshalIndent(list, "", " "); err == nil {
os.WriteFile(s.file, data, 0644)
}
}
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(v)
}
func (s *Store) list(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
defer s.mu.Unlock()
list := make([]Item, 0, len(s.items))
for _, it := range s.items {
list = append(list, it)
}
writeJSON(w, http.StatusOK, list)
}
func (s *Store) create(w http.ResponseWriter, r *http.Request) {
var in Item
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
return
}
s.mu.Lock()
defer s.mu.Unlock()
in.ID = s.nextID
s.nextID++
s.items[in.ID] = in
s.save()
writeJSON(w, http.StatusCreated, in)
}
func (s *Store) get(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
s.mu.Lock()
defer s.mu.Unlock()
it, ok := s.items[id]
if !ok {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
return
}
writeJSON(w, http.StatusOK, it)
}
func (s *Store) update(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
var in Item
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
return
}
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.items[id]; !ok {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
return
}
in.ID = id
s.items[id] = in
s.save()
writeJSON(w, http.StatusOK, in)
}
func (s *Store) remove(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.items[id]; !ok {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
return
}
delete(s.items, id)
s.save()
writeJSON(w, http.StatusOK, map[string]int{"deleted": id})
}
func main() {
store := NewStore("data.json")
mux := http.NewServeMux()
mux.HandleFunc("GET /items", store.list)
mux.HandleFunc("POST /items", store.create)
mux.HandleFunc("GET /items/{id}", store.get)
mux.HandleFunc("PUT /items/{id}", store.update)
mux.HandleFunc("DELETE /items/{id}", store.remove)
addr := ":" + os.Getenv("SERVER_PORT")
println("Listening on", addr)
http.ListenAndServe(addr, mux)
}
Step 2 — How it works
- Routing (Go 1.22). The standard
http.ServeMuxnow understands method + path patterns like"GET /items/{id}", andr.PathValue("id")reads the placeholder. That's whygo.modsaysgo 1.22— this routing needs it. No third-party router required. - The store is a map + a mutex.
items map[int]Itemholds the data;sync.Mutexguards it so two simultaneous requests can't corrupt the map. A web server handles requests concurrently, so this lock is not optional. - Persistence.
load()readsdata.jsonat startup (and rebuildsnextID);save()writes the whole map back after every change. So the store survives a restart — stop and start the server and your items are still there. - The port rule.
addr := ":" + os.Getenv("SERVER_PORT")binds every interface on the public port — the empty host means "listen on all", which is what makes the API reachable. See Your first web app.
| Endpoint | Method | Does |
|---|---|---|
/items |
GET | List all items |
/items |
POST | Create one (assigns id, returns 201) |
/items/{id} |
GET | Fetch one, or 404 |
/items/{id} |
PUT | Replace one, or 404 |
/items/{id} |
DELETE | Remove one, or 404 |
Step 3 — Build, upload, and run
Nothing compiles on the server, so build a Linux binary named app on your computer and put it on the server. From the project folder:
GOOS=linux GOARCH=amd64 go build -o app
That produces one self-contained app file (Go links everything in — no runtime to ship). Upload it into your server's root folder with the File Manager or over SFTP, then press Start. Prefer pushing source over uploading binaries? Connect the Git page and add a go build -o app post-deploy command so each deploy compiles a fresh binary. Both paths — and the exact steps — are in Go on Falix.
⚠️ Heads up: Build for
linux/amd64. A Windows or Mac build won't run on the server (exec format error), and there's no compiler on the server to fix it.
Step 4 — Verify it works
Start the server; the console prints Listening on :NNNNN and stays running. Exercise the API from any client:
# Create, then list
curl -X POST -d '{"title":"Write guide"}' http://YOUR_ADDRESS:PORT/items
curl http://YOUR_ADDRESS:PORT/items
# Update and delete
curl -X PUT -d '{"title":"Write guide","done":true}' http://YOUR_ADDRESS:PORT/items/1
curl -X DELETE http://YOUR_ADDRESS:PORT/items/2
To confirm persistence, add an item, then restart the server — list /items again and it's still there, reloaded from data.json.
Where the data lives
data.json sits in the server's file system next to app.
⚠️ Heads up: A reinstall — or switching applications — wipes the server's files, both
data.jsonand theappbinary. The binary you rebuild and re-upload (or a Git deploy rebuilds); the data you don't get back, so back it up if it matters. A single JSON file is ideal for a small service; for real scale or durability, move to a managed database — Go'sdatabase/sqlconnects with the string the panel gives you.
Troubleshooting
./app: not foundor a permission error on start — there's noappbinary in the root, or it's the wrong OS. Rebuild withGOOS=linux GOARCH=amd64 go build -o app.exec format error— the binary is for the wrong architecture; build forlinux/amd64.- Build fails with an unknown routing pattern — your local Go is older than 1.22. Update Go, or the
"GET /items"patterns won't compile. - Starts then stops with no error — a program whose
mainreturns exits cleanly; a web server stays up because it blocks onListenAndServe. See My app won't start. - Runs but the API is unreachable — a port or bind issue: I can't reach my app.
The net/http package documents everything past this — routing, middleware, timeouts — at pkg.go.dev/net/http.