Skip to content

Demo

The cmd/poc directory contains a self-contained runnable server that demonstrates the most common gorestapi patterns: two resources, a create-only struct, ownership guards, and server-side filters. The walkthrough below follows that code.

Resources

Two structs are defined — User and Note. Each has a paired _Draft type used as the create payload; gocrud strips the underscore suffix to map it to the same table.

type User struct {
    ID         uint64
    Name       string `crud:"req len:1,100"`
    Email      string `crud:"req email uniq"`
    CreatedAt  int64
    CreatedBy  uint64
    ModifiedAt int64
    ModifiedBy uint64
}

// User_Draft is the create payload — maps to the "user" table.
type User_Draft struct {
    ID    uint64
    Name  string `crud:"req len:1,100"`
    Email string `crud:"req email uniq"`
}

type Note struct {
    ID         uint64
    Title      string `crud:"req len:1,200"`
    Content    string
    Comment    string
    UserID     uint64 `crud:"req"`
    CreatedAt  int64
    CreatedBy  uint64
    ModifiedAt int64
    ModifiedBy uint64
}

// Note_Draft is the create payload — maps to the "note" table.
type Note_Draft struct {
    ID      uint64
    Title   string `crud:"req len:1,200"`
    Content string
    Comment string
    UserID  uint64 `crud:"req"`
}

The _Draft types intentionally omit audit fields (CreatedAt, ModifiedAt, etc.) so clients cannot set them directly.

Database and service

db, _ := sql.Open("sqlite", dbPath)

svc := svccrud.New(map[string]func() interface{}{
    "users": func() interface{} { return &User{} },
    "notes": func() interface{} { return &Note{} },
}, db, gocrud.DialectSQLite)

svc.CreateTables(context.Background())

CreateTables creates both tables if they do not already exist. The service registry maps URL path segments to their constructors.

Hooks

Two hooks shape note behavior.

noteComment stamps every create and update with a server-controlled Comment field that clients cannot override:

noteComment := func(obj interface{}, _ *http.Request) error {
    switch n := obj.(type) {
    case *Note:
        n.Comment = "Added with API"
    case *Note_Draft:
        n.Comment = "Added with API"
    }
    return nil
}

noteOwner rejects update and delete requests from users whose X-User-ID header does not match the note's stored UserID:

noteOwner := func(obj interface{}, r *http.Request) error {
    note := obj.(*Note)
    headerUserID, _ := strconv.ParseUint(r.Header.Get("X-User-ID"), 10, 64)
    if note.UserID != headerUserID {
        return errors.New("not the note owner")
    }
    return nil
}

AllowUpdate receives the stored record before the request body is applied, so noteOwner always checks the original owner rather than a value the client could forge.

Handler

handler := gorestapi.New(svc, gorestapi.Options{
    UserIDFunc: func(r *http.Request) uint64 {
        id, _ := strconv.ParseUint(r.Header.Get("X-User-ID"), 10, 64)
        return id
    },
    Routes: map[string]gorestapi.Route{
        "users": {
            CreateConstructor: func() interface{} { return &User_Draft{} },
        },
        "notes": {
            CreateConstructor: func() interface{} { return &Note_Draft{} },
            AllowedFilters:    []string{"UserID"},
            AllowUpdate:       noteOwner,
            AllowDelete:       noteOwner,
            PreCreate:         noteComment,
            PreUpdate:         noteComment,
            PostRead: func(obj interface{}, _ *http.Request) error {
                obj.(*Note).Comment = "Returned from gorestapi"
                return nil
            },
            PostListItem: func(obj interface{}, _ *http.Request) error {
                obj.(*Note).Comment = "Returned from gorestapi"
                return nil
            },
            FilterList: func(r *http.Request) gorestapi.FilterSet {
                return gorestapi.FilterSet{
                    Vals: map[string]string{"UserID": r.Header.Get("X-User-ID")},
                }
            },
            FilterRead: func(r *http.Request) gorestapi.FilterSet {
                return gorestapi.FilterSet{
                    Vals: map[string]string{"UserID": r.Header.Get("X-User-ID")},
                }
            },
        },
    },
})

FilterList and FilterRead automatically scope every notes query to the caller's user ID, so a user can only list and read their own notes. Injected filters take precedence over any client-supplied filter_val_* parameters.

Mounting

mux := http.NewServeMux()
mux.Handle("/api/", http.StripPrefix("/api", http.HandlerFunc(handler.Serve)))
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    fmt.Fprintln(w, "ok")
})

http.ListenAndServe(":"+port, mux)

Running

DB_PATH=poc.db PORT=8080 go run ./cmd/poc
Env var Default Description
DB_PATH poc.db Path to the SQLite database file
PORT 8080 Port the server listens on

Trying it out

# Create a user
curl -s -X PUT http://localhost:8080/api/users/ \
  -H 'Content-Type: application/json' \
  -d '{"Name":"Alice","Email":"alice@example.com"}' | jq

# Create a note owned by user 1
curl -s -X PUT http://localhost:8080/api/notes/ \
  -H 'Content-Type: application/json' \
  -H 'X-User-ID: 1' \
  -d '{"Title":"Hello","Content":"World","UserID":1}' | jq

# List notes — only the caller's notes are returned
curl -s http://localhost:8080/api/notes/ \
  -H 'X-User-ID: 1' | jq

# Read a single note (404 if UserID doesn't match)
curl -s http://localhost:8080/api/notes/1 \
  -H 'X-User-ID: 1' | jq

# Update a note (403 if not the owner)
curl -s -X PUT http://localhost:8080/api/notes/1 \
  -H 'Content-Type: application/json' \
  -H 'X-User-ID: 1' \
  -d '{"Title":"Updated"}' | jq

# Delete a note (403 if not the owner)
curl -s -X DELETE http://localhost:8080/api/notes/1 \
  -H 'X-User-ID: 1' | jq
Request Behaviour
GET /api/notes/ Returns only the caller's notes (UserID injected from header)
GET /api/notes/1 Returns 404 if the note's UserID does not match the header
PUT /api/notes/ Creates a note; Comment is stamped by PreCreate
PUT /api/notes/1 Updates a note; returns 403 if the caller is not the owner
DELETE /api/notes/1 Deletes a note; returns 403 if the caller is not the owner