Only show guides for your app
A complete JSON REST API in pure Go standard library — net/http routing, an in-memory store guarded by a mutex, persisted to a JSON file so data survives restarts. No dependencies, verified building and serving.
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.
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)
}
http.ServeMux now understands method + path patterns like "GET /items/{id}", and r.PathValue("id") reads the placeholder. That's why go.mod says go 1.22 — this routing needs it. No third-party router required.items map[int]Item holds the data; sync.Mutex guards it so two simultaneous requests can't corrupt the map. A web server handles requests concurrently, so this lock is not optional.load() reads data.json at startup (and rebuilds nextID); 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.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 |
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.
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.
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.
./app: not found or a permission error on start — there's no app binary in the root, or it's the wrong OS. Rebuild with GOOS=linux GOARCH=amd64 go build -o app.exec format error — the binary is for the wrong architecture; build for linux/amd64."GET /items" patterns won't compile.main returns exits cleanly; a web server stays up because it blocks on ListenAndServe. See My app won't start.The net/http package documents everything past this — routing, middleware, timeouts — at pkg.go.dev/net/http.