Files
ResolveSpec/pkg/restheadspec
..
2025-12-08 16:56:48 +02:00
2025-11-20 14:30:59 +02:00
2025-11-11 11:03:02 +02:00
2025-12-08 16:56:48 +02:00
2025-12-30 15:30:23 +00:00
2025-12-08 16:56:48 +02:00
2025-12-30 14:12:07 +02:00
2025-11-10 10:22:55 +02:00
2025-12-12 09:45:44 +02:00
2025-12-19 16:52:34 +02:00
2025-12-30 17:44:57 +02:00
2025-12-08 16:56:48 +02:00
2025-12-09 12:01:21 +02:00
2025-11-20 12:47:36 +02:00
2025-11-20 12:47:36 +02:00

RestHeadSpec - Header-Based REST API

RestHeadSpec provides a REST API where all query options are passed via HTTP headers instead of the request body. This provides cleaner separation between data and metadata, making it ideal for GET requests and RESTful architectures.

Features

  • Header-Based Querying: All query options via HTTP headers
  • Lifecycle Hooks: Before/after hooks for create, read, update, delete operations
  • Cursor Pagination: Efficient cursor-based pagination with complex sorting
  • Advanced Filtering: Field filters, search operators, AND/OR logic
  • Multiple Response Formats: Simple, detailed, and Syncfusion-compatible responses
  • Single Record as Object: Automatically return single-element arrays as objects (default)
  • Base64 Support: Base64-encoded header values for complex queries
  • Type-Aware Filtering: Automatic type detection and conversion
  • CORS Support: Comprehensive CORS headers for cross-origin requests
  • OPTIONS Method: Full OPTIONS support for CORS preflight

Quick Start

Setup with GORM

import "github.com/bitechdev/ResolveSpec/pkg/restheadspec"
import "github.com/gorilla/mux"

// Create handler
handler := restheadspec.NewHandlerWithGORM(db)

// IMPORTANT: Register models BEFORE setting up routes
handler.Registry.RegisterModel("public.users", &User{})
handler.Registry.RegisterModel("public.posts", &Post{})

// Setup routes
router := mux.NewRouter()
restheadspec.SetupMuxRoutes(router, handler, nil)

// Start server
http.ListenAndServe(":8080", router)

Setup with Bun ORM

import "github.com/bitechdev/ResolveSpec/pkg/restheadspec"
import "github.com/uptrace/bun"

// Create handler with Bun
handler := restheadspec.NewHandlerWithBun(bunDB)

// Register models
handler.Registry.RegisterModel("public.users", &User{})

// Setup routes (same as GORM)
router := mux.NewRouter()
restheadspec.SetupMuxRoutes(router, handler, nil)

Basic Usage

Simple GET Request

GET /public/users HTTP/1.1
Host: api.example.com
X-Select-Fields: id,name,email
X-FieldFilter-Status: active
X-Sort: -created_at
X-Limit: 50

With Preloading

GET /public/users HTTP/1.1
X-Select-Fields: id,name,email,department_id
X-Preload: department:id,name
X-FieldFilter-Status: active
X-Limit: 50

Common Headers

Header Description Example
X-Select-Fields Columns to include id,name,email
X-Not-Select-Fields Columns to exclude password,internal_notes
X-FieldFilter-{col} Exact match filter X-FieldFilter-Status: active
X-SearchFilter-{col} Fuzzy search (ILIKE) X-SearchFilter-Name: john
X-SearchOp-{op}-{col} Filter with operator X-SearchOp-Gte-Age: 18
X-Preload Preload relations posts:id,title
X-Sort Sort columns -created_at,+name
X-Limit Limit results 50
X-Offset Offset for pagination 100
X-Clean-JSON Remove null/empty fields true
X-Single-Record-As-Object Return single records as objects false

Available Operators: eq, neq, gt, gte, lt, lte, contains, startswith, endswith, between, betweeninclusive, in, empty, notempty

For complete header documentation, see HEADERS.md.

Lifecycle Hooks

RestHeadSpec supports lifecycle hooks for all CRUD operations:

import "github.com/bitechdev/ResolveSpec/pkg/restheadspec"

// Create handler
handler := restheadspec.NewHandlerWithGORM(db)

// Register a before-read hook (e.g., for authorization)
handler.Hooks.Register(restheadspec.BeforeRead, func(ctx *restheadspec.HookContext) error {
    // Check permissions
    if !userHasPermission(ctx.Context, ctx.Entity) {
        return fmt.Errorf("unauthorized access to %s", ctx.Entity)
    }

    // Modify query options
    ctx.Options.Limit = ptr(100) // Enforce max limit

    return nil
})

// Register an after-read hook (e.g., for data transformation)
handler.Hooks.Register(restheadspec.AfterRead, func(ctx *restheadspec.HookContext) error {
    // Transform or filter results
    if users, ok := ctx.Result.([]User); ok {
        for i := range users {
            users[i].Email = maskEmail(users[i].Email)
        }
    }
    return nil
})

// Register a before-create hook (e.g., for validation)
handler.Hooks.Register(restheadspec.BeforeCreate, func(ctx *restheadspec.HookContext) error {
    // Validate data
    if user, ok := ctx.Data.(*User); ok {
        if user.Email == "" {
            return fmt.Errorf("email is required")
        }
        // Add timestamps
        user.CreatedAt = time.Now()
    }
    return nil
})

Available Hook Types:

  • BeforeRead, AfterRead
  • BeforeCreate, AfterCreate
  • BeforeUpdate, AfterUpdate
  • BeforeDelete, AfterDelete

HookContext provides:

  • Context: Request context
  • Handler: Access to handler, database, and registry
  • Schema, Entity, TableName: Request info
  • Model: The registered model type
  • Options: Parsed request options (filters, sorting, etc.)
  • ID: Record ID (for single-record operations)
  • Data: Request data (for create/update)
  • Result: Operation result (for after hooks)
  • Writer: Response writer (allows hooks to modify response)

Cursor Pagination

RestHeadSpec supports efficient cursor-based pagination for large datasets:

GET /public/posts HTTP/1.1
X-Sort: -created_at,+id
X-Limit: 50
X-Cursor-Forward: <cursor_token>

How it works:

  1. First request returns results + cursor token in response
  2. Subsequent requests use X-Cursor-Forward or X-Cursor-Backward
  3. Cursor maintains consistent ordering even with data changes
  4. Supports complex multi-column sorting

Benefits over offset pagination:

  • Consistent results when data changes
  • Better performance for large offsets
  • Prevents "skipped" or duplicate records
  • Works with complex sort expressions

Example with hooks:

// Enable cursor pagination in a hook
handler.Hooks.Register(restheadspec.BeforeRead, func(ctx *restheadspec.HookContext) error {
    // For large tables, enforce cursor pagination
    if ctx.Entity == "posts" && ctx.Options.Offset != nil && *ctx.Options.Offset > 1000 {
        return fmt.Errorf("use cursor pagination for large offsets")
    }
    return nil
})

Response Formats

RestHeadSpec supports multiple response formats:

1. Simple Format (X-SimpleApi: true):

[
  { "id": 1, "name": "John" },
  { "id": 2, "name": "Jane" }
]

2. Detail Format (X-DetailApi: true, default):

{
  "success": true,
  "data": [...],
  "metadata": {
    "total": 100,
    "filtered": 100,
    "limit": 50,
    "offset": 0
  }
}

3. Syncfusion Format (X-Syncfusion: true):

{
  "result": [...],
  "count": 100
}

Single Record as Object (Default Behavior)

By default, RestHeadSpec automatically converts single-element arrays into objects for cleaner API responses.

Default behavior (enabled):

GET /public/users/123
{
  "success": true,
  "data": { "id": 123, "name": "John", "email": "john@example.com" }
}

To disable (force arrays):

GET /public/users/123
X-Single-Record-As-Object: false
{
  "success": true,
  "data": [{ "id": 123, "name": "John", "email": "john@example.com" }]
}

How it works:

  • When a query returns exactly one record, it's returned as an object
  • When a query returns multiple records, they're returned as an array
  • Set X-Single-Record-As-Object: false to always receive arrays
  • Works with all response formats (simple, detail, syncfusion)
  • Applies to both read operations and create/update returning clauses

CORS & OPTIONS Support

RestHeadSpec includes comprehensive CORS support for cross-origin requests:

OPTIONS Method:

OPTIONS /public/users HTTP/1.1

Returns metadata with appropriate CORS headers:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Select-Fields, X-FieldFilter-*, ...
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true

Key Features:

  • OPTIONS returns model metadata (same as GET metadata endpoint)
  • All HTTP methods include CORS headers automatically
  • OPTIONS requests don't require authentication (CORS preflight)
  • Supports all HeadSpec custom headers (X-Select-Fields, X-FieldFilter-*, etc.)
  • 24-hour max age to reduce preflight requests

Configuration:

import "github.com/bitechdev/ResolveSpec/pkg/common"

// Get default CORS config
corsConfig := common.DefaultCORSConfig()

// Customize if needed
corsConfig.AllowedOrigins = []string{"https://example.com"}
corsConfig.AllowedMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}

Advanced Features

Base64 Encoding

For complex header values, use base64 encoding:

GET /public/users HTTP/1.1
X-Select-Fields-Base64: aWQsbmFtZSxlbWFpbA==

AND/OR Logic

Combine multiple filters with AND/OR logic:

GET /public/users HTTP/1.1
X-FieldFilter-Status: active
X-SearchOp-Gte-Age: 18
X-Filter-Logic: AND

Complex Preloading

Load nested relationships:

GET /public/users HTTP/1.1
X-Preload: posts:id,title,comments:id,text,author:name

Model Registration

type User struct {
    ID    uint   `json:"id" gorm:"primaryKey"`
    Name  string `json:"name"`
    Email string `json:"email"`
    Posts []Post `json:"posts,omitempty" gorm:"foreignKey:UserID"`
}

// Schema.Table format
handler.Registry.RegisterModel("public.users", &User{})

Complete Example

package main

import (
    "log"
    "net/http"

    "github.com/bitechdev/ResolveSpec/pkg/restheadspec"
    "github.com/gorilla/mux"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type User struct {
    ID     uint   `json:"id" gorm:"primaryKey"`
    Name   string `json:"name"`
    Email  string `json:"email"`
    Status string `json:"status"`
}

func main() {
    // Connect to database
    db, err := gorm.Open(postgres.Open("your-connection-string"), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }

    // Create handler
    handler := restheadspec.NewHandlerWithGORM(db)

    // Register models
    handler.Registry.RegisterModel("public.users", &User{})

    // Add hooks
    handler.Hooks.Register(restheadspec.BeforeRead, func(ctx *restheadspec.HookContext) error {
        log.Printf("Reading %s", ctx.Entity)
        return nil
    })

    // Setup routes
    router := mux.NewRouter()
    restheadspec.SetupMuxRoutes(router, handler, nil)

    // Start server
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", router))
}

Testing

RestHeadSpec is designed for testability:

import (
    "net/http/httptest"
    "testing"
)

func TestUserRead(t *testing.T) {
    handler := restheadspec.NewHandlerWithGORM(testDB)
    handler.Registry.RegisterModel("public.users", &User{})

    req := httptest.NewRequest("GET", "/public/users", nil)
    req.Header.Set("X-Select-Fields", "id,name")
    req.Header.Set("X-Limit", "10")

    rec := httptest.NewRecorder()
    // Test your handler...
}

See Also

License

This package is part of ResolveSpec and is licensed under the MIT License.