Basic REST example
This page shows a small-but-realistic REST API built with jetwarp.
It is intentionally “basic” in dependencies (stdlib-friendly), but not trivial in structure. It demonstrates:
- a versioned API prefix (
/api/v1) viaGroup - public and protected endpoints side-by-side via
With - portable net/http middleware (request id, recovery, logging, timeouts)
- canonical
{name}path parameters read via(*http.Request).PathValue - “no panic” registration rules and a fail-fast
Err()check
If you want the smallest possible hello-world, start with Quickstart. This page is meant to be the next step: a reference you can adapt for real services.
Why this example looks the way it does
1) Keep the application surface stable
Jetwarp’s goal is that your service code registers routes against one API (adapter.Adapter) and can swap the
underlying router backend later (stdlib/chi/gin/echo/fiber) without rewriting your routing setup.
So this example avoids framework-specific handler types and sticks to net/http everywhere.
2) Use portable middleware by default
Router ecosystems often come with their own middleware APIs. That’s convenient, but not portable.
This example uses only portable net/http middleware wrapped with adapter.HTTPNamed(...) so it behaves consistently
across adapters.
3) Treat route registration as “config time”
Invalid patterns, unsupported features, or driver failures should not crash your process.
Jetwarp accumulates errors and keeps going, which is helpful during iterative setup — but it makes the final Err()
check mandatory.
The example API
We’ll implement a minimal “students” API with an in-memory store:
Public:
GET /api/v1/healthzGET /api/v1/studentsGET /api/v1/students/{id}
Protected (Bearer token):
POST /api/v1/studentsPUT /api/v1/students/{id}DELETE /api/v1/students/{id}
Full example (single file)
You can paste this into main.go and run it.
Note: For a real service, you’d split storage, middleware, and handlers into packages. Here we keep it in one file so it’s easy to copy and tweak.
package main
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"log"
"net/http"
"strings"
"sync"
"time"
"codeberg.org/iaconlabs/jetwarp/adapter"
"codeberg.org/iaconlabs/jetwarp/adapter/chi"
)
// -----------------------------------------------------------------------------
// Model + in-memory store
// -----------------------------------------------------------------------------
type Student struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type Store struct {
mu sync.RWMutex
m map[string]Student
}
func NewStore() *Store {
return &Store{
m: map[string]Student{
"1": {ID: "1", Name: "Ada Lovelace", Email: "ada@example.com"},
"2": {ID: "2", Name: "Alan Turing", Email: "alan@example.com"},
},
}
}
func (s *Store) List() []Student {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]Student, 0, len(s.m))
for _, v := range s.m {
out = append(out, v)
}
return out
}
func (s *Store) Get(id string) (Student, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
v, ok := s.m[id]
return v, ok
}
func (s *Store) Create(v Student) (Student, error) {
if strings.TrimSpace(v.Name) == "" || strings.TrimSpace(v.Email) == "" {
return Student{}, errors.New("name and email are required")
}
s.mu.Lock()
defer s.mu.Unlock()
v.ID = newID()
s.m[v.ID] = v
return v, nil
}
func (s *Store) Update(id string, patch Student) (Student, error) {
s.mu.Lock()
defer s.mu.Unlock()
cur, ok := s.m[id]
if !ok {
return Student{}, errors.New("not found")
}
if strings.TrimSpace(patch.Name) != "" {
cur.Name = patch.Name
}
if strings.TrimSpace(patch.Email) != "" {
cur.Email = patch.Email
}
s.m[id] = cur
return cur, nil
}
func (s *Store) Delete(id string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.m[id]; !ok {
return false
}
delete(s.m, id)
return true
}
// -----------------------------------------------------------------------------
// Middleware (portable net/http)
// -----------------------------------------------------------------------------
type ctxKey string
const (
ctxReqID ctxKey = "reqid"
headerReqID = "X-Request-ID"
headerAuth = "Authorization"
bearerPrefix = "Bearer "
defaultAuthToken = "dev-token" // demo only
)
func RequestID() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := strings.TrimSpace(r.Header.Get(headerReqID))
if id == "" {
id = newReqID()
}
w.Header().Set(headerReqID, id)
ctx := context.WithValue(r.Context(), ctxReqID, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func RecoverJSON() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if recover() != nil {
writeJSON(w, http.StatusInternalServerError, map[string]any{
"error": "internal server error",
})
}
}()
next.ServeHTTP(w, r)
})
}
}
func Timeout(d time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), d)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// logRW wraps http.ResponseWriter to capture the status code for logging.
type logRW struct {
http.ResponseWriter
status int
}
func (w *logRW) WriteHeader(statusCode int) {
w.status = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
func AccessLog(logf func(format string, args ...any)) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lw := &logRW{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(lw, r)
dur := time.Since(start)
reqID, _ := r.Context().Value(ctxReqID).(string)
if reqID != "" {
logf("[%s] %s %s -> %d (%s)", reqID, r.Method, r.URL.Path, lw.status, dur)
} else {
logf("%s %s -> %d (%s)", r.Method, r.URL.Path, lw.status, dur)
}
})
}
}
// AuthBearer is intentionally minimal (good for internal tools and prototypes).
func AuthBearer(token string) func(http.Handler) http.Handler {
token = strings.TrimSpace(token)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
raw := strings.TrimSpace(r.Header.Get(headerAuth))
if !strings.HasPrefix(raw, bearerPrefix) {
writeJSON(w, http.StatusUnauthorized, map[string]any{"error": "missing bearer token"})
return
}
got := strings.TrimSpace(strings.TrimPrefix(raw, bearerPrefix))
if got == "" || got != token {
writeJSON(w, http.StatusForbidden, map[string]any{"error": "invalid token"})
return
}
next.ServeHTTP(w, r)
})
}
}
// -----------------------------------------------------------------------------
// Handlers
// -----------------------------------------------------------------------------
type API struct {
store *Store
}
func (a *API) Healthz(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}
func (a *API) ListStudents(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, a.store.List())
}
func (a *API) GetStudent(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "missing id"})
return
}
v, ok := a.store.Get(id)
if !ok {
writeJSON(w, http.StatusNotFound, map[string]any{"error": "not found"})
return
}
writeJSON(w, http.StatusOK, v)
}
func (a *API) CreateStudent(w http.ResponseWriter, r *http.Request) {
var in Student
if err := readJSON(r, &in); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
return
}
out, err := a.store.Create(in)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
return
}
writeJSON(w, http.StatusCreated, out)
}
func (a *API) UpdateStudent(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var patch Student
if err := readJSON(r, &patch); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
return
}
out, err := a.store.Update(id, patch)
if err != nil {
status := http.StatusBadRequest
if err.Error() == "not found" {
status = http.StatusNotFound
}
writeJSON(w, status, map[string]any{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, out)
}
func (a *API) DeleteStudent(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if ok := a.store.Delete(id); !ok {
writeJSON(w, http.StatusNotFound, map[string]any{"error": "not found"})
return
}
w.WriteHeader(http.StatusNoContent)
}
// -----------------------------------------------------------------------------
// JSON helpers
// -----------------------------------------------------------------------------
func readJSON(r *http.Request, dst any) error {
if r.Body == nil {
return errors.New("empty body")
}
defer r.Body.Close()
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
return dec.Decode(dst)
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
enc := json.NewEncoder(w)
enc.SetEscapeHTML(true)
_ = enc.Encode(v)
}
func newReqID() string {
var b [12]byte
_, _ = rand.Read(b[:])
return hex.EncodeToString(b[:])
}
func newID() string {
return newReqID()[:8]
}
// -----------------------------------------------------------------------------
// main
// -----------------------------------------------------------------------------
func main() {
r := chi.New()
api := &API{store: NewStore()}
r.Use(
adapter.HTTPNamed("recover_json", RecoverJSON()),
adapter.HTTPNamed("request_id", RequestID()),
adapter.HTTPNamed("access_log", AccessLog(log.Printf)),
adapter.HTTPNamed("timeout_5s", Timeout(5*time.Second)),
)
v1 := r.Group("/api").Group("/v1")
// Public endpoints.
v1.HandleFunc(http.MethodGet, "/healthz", api.Healthz)
v1.HandleFunc(http.MethodGet, "/students", api.ListStudents)
v1.HandleFunc(http.MethodGet, "/students/{id}", api.GetStudent)
// Protected endpoints.
private := v1.With(adapter.HTTPNamed("auth", AuthBearer(defaultAuthToken)))
private.HandleFunc(http.MethodPost, "/students", api.CreateStudent)
private.HandleFunc(http.MethodPut, "/students/{id}", api.UpdateStudent)
private.HandleFunc(http.MethodDelete, "/students/{id}", api.DeleteStudent)
// Fail fast on configuration errors.
if err := r.Err(); err != nil {
log.Fatal(err)
}
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", r))
}
How to test it quickly
# Health check
curl -i http://localhost:8080/api/v1/healthz
# List
curl -i http://localhost:8080/api/v1/students
# Get
curl -i http://localhost:8080/api/v1/students/1
# Create (requires token)
curl -i -X POST http://localhost:8080/api/v1/students -H 'Authorization: Bearer dev-token' -H 'Content-Type: application/json' -d '{"name":"Grace Hopper","email":"grace@example.com"}'
Small things to adjust for real services
This example keeps things minimal on purpose. In a real service you may want to add:
- request size limits and strict JSON limits
- structured logging (slog) + tracing correlation
- a proper auth mechanism (JWT / mTLS / session)
- a real persistence layer instead of an in-memory map
Even with those changes, the overall structure tends to stay the same: portable middleware, Group for structure,
With for policy boundaries, and a strict Err() check before serving.