🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🕵️ SicherheitslückenBurn Out, Or Fade Away(14.09.2026 um 14:25 Uhr)
🪟 Windows TippsKB5129194 Windows 11 26H1 Out of Band Update - Deskmodder.de(14.09.2026 um 19:25 Uhr)
🪟 Windows TippsGoogle Gemini: Neue Windows-App holt die KI aus dem Browser(14.09.2026 um 06:00 Uhr)
🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🕵️ SicherheitslückenBurn Out, Or Fade Away(14.09.2026 um 14:25 Uhr)
🪟 Windows TippsKB5129194 Windows 11 26H1 Out of Band Update - Deskmodder.de(14.09.2026 um 19:25 Uhr)
🪟 Windows TippsGoogle Gemini: Neue Windows-App holt die KI aus dem Browser(14.09.2026 um 06:00 Uhr)

🔧 Programmierung 🕛 vor 6 Monaten 5 Min Lesezeit
0

Building a Clean REST API in Go — No Frameworks, No Fuss

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Go's standard library is powerful enough to build a production-ready REST API without reaching for a framework. In this post, we'll walk through a small but well-structured Notes API that covers all CRUD operations — and the architectural decisions behind it.






What we're building



A simple API to manage notes, with five routes:






































Method Path Description
POST /notes Create a note
GET /notes List all notes
GET /notes/{id} Get a note by ID
PUT /notes/{id} Update a note
DELETE /notes/{id} Delete a note








Project structure






CODE
notes-api/
├── cmd/api/ # Entry point
├── internal/
│ ├── httpapi/ # HTTP handlers
│ ├── note/ # Model, service, storage interface
│ └── storage/ # In-memory implementation






Three distinct layers: HTTP, business logic, and storage. Each one

knows nothing about the others except through interfaces.







The model





CODE
type Note struct {
ID int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}





Simple and flat. Timestamps are managed by the service, not by the caller.







The storage interface





CODE
type Store interface {
Create(ctx context.Context, n Note) (Note, error)
List(ctx context.Context) ([]Note, error)
GetByID(ctx context.Context, id int64) (Note, error)
Update(ctx context.Context, id int64, title, content string) (Note, error)
Delete(ctx context.Context, id int64) error
}





Defining a Store interface in the note package (not in storage) is a key decision: the business logic owns the contract, and the storage implementation satisfies it. This makes swapping backends (PostgreSQL, Redis, etc.) trivial later.







The service layer





CODE
var ErrInvalidTitle = errors.New("invalid title")
var ErrNotFound = errors.New("not found")

func (s *Service) Create(ctx context.Context, title, content string) (Note, error) {
if title == "" {
return Note{}, ErrInvalidTitle
}
now := time.Now()
n := Note{
Title: title,
Content: content,
CreatedAt: now,
UpdatedAt: now,
}
return s.store.Create(ctx, n)
}





The service is where business rules live. Here it validates that the title is non-empty and stamps the timestamps before delegating to the store. Typed sentinel errors (ErrInvalidTitle, ErrNotFound) let the HTTP layer map them to the right status codes without leaking implementation details.







The in-memory storage





CODE
type MemoryStorage struct {
mu sync.RWMutex
notes []note.Note
nextID int64
}





A slice protected by a sync.RWMutex. Read operations (List, GetByID) use RLock to allow concurrent reads; write operations use Lock for exclusive access. Note that the returned slice from List is a copy — so callers can't accidentally mutate the internal state:




CODE
func (s *MemoryStorage) List(ctx context.Context) ([]note.Note, error) {
s.mu.RLock()
defer s.mu.RUnlock()

out := make([]note.Note, len(s.notes))
copy(out, s.notes)
return out, nil
}












The HTTP handler



Routing is done with the standard http.ServeMux. Two handler functions cover

all five routes:




CODE
func (h *Handler) Register(mux *http.ServeMux) {
mux.HandleFunc("/notes", h.handleNotes)
mux.HandleFunc("/notes/", h.handleNoteByID)
}






/notes handles GET (list) and POST (create). /notes/ catches everything

with an ID suffix and dispatches on the method:




CODE
func (h *Handler) handleNoteByID(w http.ResponseWriter, r *http.Request) {
id, err := parseIDFromPath(r.URL.Path)
if err != nil {
http.Error(w, "Invalid note ID", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
h.getNote(w, r, id)
case http.MethodPut:
h.updateNote(w, r, id)
case http.MethodDelete:
h.deleteNote(w, r, id)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}






Service errors are translated to HTTP status codes in one place:




CODE
func handleServiceError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, note.ErrInvalidTitle):
http.Error(w, "Invalid title", http.StatusBadRequest)
case errors.Is(err, note.ErrNotFound):
http.Error(w, "Note not found", http.StatusNotFound)
default:
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}












Wiring it all together






CODE
func main() {
store := storage.NewMemoryStore()
service := note.NewService(store)
handler := httpapi.NewHandler(service)

mux := http.NewServeMux()
handler.Register(mux)

log.Println("Server is running on port 8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
}






Three lines of wiring. Each dependency is injected explicitly — no globals, no service locators.









Try it out






CODE
go run ./cmd/api

# Create
curl -X POST http://localhost:8080/notes \
-H "Content-Type: application/json" \
-d '{"title": "Hello", "content": "World"}'

# List
curl http://localhost:8080/notes

# Get
curl http://localhost:8080/notes/1

# Update
curl -X PUT http://localhost:8080/notes/1 \
-H "Content-Type: application/json" \
-d '{"title": "Updated", "content": "New content"}'

# Delete
curl -X DELETE http://localhost:8080/notes/1












Key takeaways





  • No framework needed for a simple CRUD API — net/http is enough.


  • Interface in the consumer package (note.Store lives in note, notstorage) keeps dependencies pointing inward.


  • Typed sentinel errors decouple business logic from HTTP concerns.


  • sync.RWMutex with defensive copies keeps the in-memory store safe under concurrency.


  • Dependency injection via constructors makes the whole thing trivially testable.



The next natural step would be swapping MemoryStorage for a real database (PostgreSQL with pgx, for example) — and because of the Store interface, main.go is the only file that changes.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
The Gemini desktop app is now available for Windows
1 Quelle
Burn Out, Or Fade Away
1 Quelle
Windows 11 KB5129195 is out after Microsoft confirms major issues with the September 2026 update, but it won’t fix AMD GPU errors
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building a Clean REST API in Go — No Frameworks, No Fuss

Thematisch verwandte Begriffe: Building, Clean, REST, Frameworks · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...