restman

module
v0.0.0-...-b05e202 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2025 License: MIT

README ΒΆ

RestMan

A declarative REST API framework for Go that automatically generates routes from structs

RestMan takes your Go structs and creates fully functional REST APIs with minimal boilerplate. Inspired by Symfony's API Platform, built on top of Gin, and designed with Go generics for type safety.

Go Version License

Features

  • πŸš€ Zero Boilerplate - Full REST API from a single struct
  • 🎯 Type-Safe Generics - Compile-time type checking with Go 1.23+ generics
  • πŸ”„ Multi-Format Support - JSON, JSON-LD (Hydra), XML, CSV, MessagePack
  • πŸ”’ Security First - Built-in firewall and fine-grained authorization
  • πŸ“¦ Multiple ORMs - GORM and MongoDB out of the box, extensible for others
  • 🎭 Serialization Groups - Control field visibility per context
  • 🌳 Nested Resources - Unlimited subresource nesting
  • ⚑ Batch Operations - Efficient bulk create/update/delete
  • πŸ“„ Pagination - Configurable pagination with Hydra metadata
  • πŸ” Sorting - Multi-field sorting with client control
  • πŸ’Ύ HTTP & Redis Caching - Cache-Control headers and Redis cache library

Table of Contents

Installation

go get github.com/philiphil/restman

Requirements:

  • Go 1.23 or higher
  • A database (SQLite, PostgreSQL, MySQL via GORM, or MongoDB)

Quick Start

1. Define Your Entity
package main

import (
    "github.com/philiphil/restman/orm/entity"
)

type Book struct {
    entity.BaseEntity
    Title       string `json:"title" groups:"read,write"`
    Author      string `json:"author" groups:"read,write"`
    ISBN        string `json:"isbn" groups:"read"`
    PublishedAt string `json:"published_at" groups:"read"`
}

func (b Book) GetId() entity.ID { return b.Id }
func (b Book) SetId(id any) entity.Entity {
    b.Id = entity.CastId(id)
    return b
}
2. Create the API
package main

import (
    "github.com/gin-gonic/gin"
    "github.com/philiphil/restman/orm"
    "github.com/philiphil/restman/orm/gormrepository"
    "github.com/philiphil/restman/route"
    "github.com/philiphil/restman/router"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

func main() {
    db, _ := gorm.Open(sqlite.Open("books.db"), &gorm.Config{})
    db.AutoMigrate(&Book{})

    r := gin.Default()

    bookRouter := router.NewApiRouter(
        *orm.NewORM(gormrepository.NewRepository[Book](db)),
        route.DefaultApiRoutes(),
    )

    bookRouter.AllowRoutes(r)

    r.Run(":8080")
}
3. Use Your API
# Create a book
curl -X POST http://localhost:8080/api/book \
  -H "Content-Type: application/json" \
  -d '{"title":"The Go Programming Language","author":"Alan Donovan"}'

# Get all books
curl http://localhost:8080/api/book

# Get a specific book
curl http://localhost:8080/api/book/1

# Update a book
curl -X PUT http://localhost:8080/api/book/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Updated Title","author":"Alan Donovan"}'

# Delete a book
curl -X DELETE http://localhost:8080/api/book/1

That's it! You now have a full REST API with:

  • GET /api/book - List all books (paginated)
  • GET /api/book/:id - Get a specific book
  • POST /api/book - Create a book
  • PUT /api/book/:id - Full update
  • PATCH /api/book/:id - Partial update
  • DELETE /api/book/:id - Delete a book
  • HEAD /api/book/:id - Check existence
  • OPTIONS /api/book - Available methods

Core Concepts

Entity Interface

Every entity must implement the entity.Entity interface:

type Entity interface {
    GetId() ID
    SetId(any) Entity
}

Use entity.BaseEntity to get this for free, along with CreatedAt, UpdatedAt, and DeletedAt.

Serialization Groups

Control field visibility using the groups tag:

type User struct {
    entity.BaseEntity
    Email    string `json:"email" groups:"read,write"`
    Password string `json:"password" groups:"write"` // Only for input
    Token    string `json:"token" groups:"admin"`    // Only for admins
}
Repository Pattern

RestMan uses a repository abstraction, allowing you to swap databases easily:

// GORM (SQL databases)
gormRepo := gormrepository.NewRepository[Book](db)

// MongoDB
mongoRepo := mongorepository.NewRepository[Book](collection)

// Custom implementation
type MyRepo struct{}
func (r MyRepo) FindAll(ctx context.Context) ([]Book, error) { ... }
Multi-Format Support

RestMan automatically negotiates content type based on the Accept header:

# JSON (default)
curl http://localhost:8080/api/book

# XML
curl -H "Accept: text/xml" http://localhost:8080/api/book

# CSV
curl -H "Accept: application/csv" http://localhost:8080/api/book

# MessagePack
curl -H "Accept: application/msgpack" http://localhost:8080/api/book

# JSON-LD with Hydra pagination
curl -H "Accept: application/ld+json" http://localhost:8080/api/book

Examples

See the example/ directory for complete working examples:

Configuration

Router-Level Configuration

Set defaults for all routes:

import "github.com/philiphil/restman/configuration"

bookRouter := router.NewApiRouter(
    *orm.NewORM(gormrepository.NewRepository[Book](db)),
    route.DefaultApiRoutes(),
    configuration.ItemPerPage(50),
    configuration.MaxItemPerPage(500),
    configuration.RoutePrefix("api", "v1"),
    configuration.RouteName("library"),
    configuration.PaginationClientControl(true),
    configuration.SortingClientControl(true),
    configuration.Sorting(map[string]string{"title": "asc"}),
    configuration.NetworkCachingPolicy(3600),
)
Route-Level Configuration

Override settings for specific operations:

routes := route.DefaultApiRoutes()

routes[route.Get].Configuration[configuration.OutputSerializationGroupsType] = configuration.OutputSerializationGroups("read", "public")
routes[route.Get].Configuration[configuration.ItemPerPageType] = configuration.ItemPerPage(100)

routes[route.Post].Configuration[configuration.InputSerializationGroupsType] = configuration.InputSerializationGroups("write")

bookRouter := router.NewApiRouter(
    *orm.NewORM(gormrepository.NewRepository[Book](db)),
    routes,
)
Pagination
# Default pagination
GET /api/book?page=2

# Custom items per page (if allowed)
GET /api/book?page=1&itemsPerPage=50
Sorting
# Sort by title ascending
GET /api/book?order[title]=asc

# Multiple field sorting
GET /api/book?order[publishedAt]=desc&order[title]=asc

Security

Authentication with Firewalls
import (
    "github.com/philiphil/restman/security"
)

type MyFirewall struct{}

func (f MyFirewall) ExtractUser(c *gin.Context) (any, *errors.ApiError) {
    token := c.GetHeader("Authorization")
    if token == "" {
        return nil, errors.NewBlockingError(errors.ErrUnauthorized, "Missing token")
    }

    user := validateToken(token) // Your validation logic
    if user == nil {
        return nil, errors.NewBlockingError(errors.ErrUnauthorized, "Invalid token")
    }

    return user, nil
}

bookRouter.SetFirewall(MyFirewall{})
Authorization
// Control read access
bookRouter.SetReadingRights(func(c *gin.Context, book Book, user any) bool {
    if book.Private && book.AuthorID != user.(User).ID {
        return false // User can't read this private book
    }
    return true
})

// Control write access
bookRouter.SetWritingRights(func(c *gin.Context, book Book, user any) bool {
    return book.AuthorID == user.(User).ID // Only author can modify
})

Advanced Usage

Subresources

Create nested resource routes:

// Creates routes like: /api/author/:id/books/:id
authorRouter := router.NewApiRouter(
    *orm.NewORM(gormrepository.NewRepository[Author](db)),
    route.DefaultApiRoutes(),
)

bookRouter := router.NewApiRouter(
    *orm.NewORM(gormrepository.NewRepository[Book](db)),
    route.DefaultApiRoutes(),
)

authorRouter.AddSubresource(bookRouter)
authorRouter.AllowRoutes(r)
Batch Operations
# Batch create
POST /api/book/batch
[
  {"title": "Book 1", "author": "Author 1"},
  {"title": "Book 2", "author": "Author 2"}
]

# Batch get by IDs
GET /api/book/batch?ids=1,2,3

# Batch update
PUT /api/book/batch
[
  {"id": 1, "title": "Updated Book 1"},
  {"id": 2, "title": "Updated Book 2"}
]

# Batch delete
DELETE /api/book/batch?ids=1,2,3
Caching

HTTP Caching (Headers)

RestMan supports HTTP caching via Cache-Control headers:

bookRouter := router.NewApiRouter(
    *orm.NewORM(gormrepository.NewRepository[Book](db)),
    route.DefaultApiRoutes(),
    configuration.NetworkCachingPolicy(3600), // Cache for 1 hour
)

This automatically sets Cache-Control: public, max-age=3600 headers on GET requests.

Model/Entity Separation

Keep your database models separate from API representations:

// Database model (internal)
type BookModel struct {
    ID          uint
    Title       string
    AuthorID    uint
    InternalRef string // Not exposed in API
}

// API entity (external)
type Book struct {
    entity.BaseEntity
    Title  string `json:"title" groups:"read,write"`
    Author Author `json:"author" groups:"read"`
}

func (b BookModel) ToEntity() Book {
    return Book{
        BaseEntity: entity.BaseEntity{Id: b.ID},
        Title:      b.Title,
        Author:     fetchAuthor(b.AuthorID),
    }
}

func (b BookModel) FromEntity(book Book) any {
    return BookModel{
        ID:       book.Id,
        Title:    book.Title,
        AuthorID: book.Author.Id,
    }
}

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request
Running Tests
# Run all tests
go test ./...

# Run tests with coverage
go test -cover ./...

# Run specific test suite
go test ./test/router/...

Roadmap

TODO/ IDEAS
  • Add random configuration to clarify behavior, suggest best practices and allow flexibility (right now clarifying backup configuration)
  • Filtering implementation
  • UUID compatibility for entity.ID
  • Force lowercase option for JSON keys
  • Automatic Redis caching integration in router
  • GraphQL support
  • Hooks system for lifecycle events
  • Built-in requireOwnership for firewall or something
  • Rate limiting middleware (Ai suggestion)
  • Audit login middleware (Ai suggestion)
  • Validation/constraints (Ai suggestion)
  • Finishing redis implementation
  • OpenAPI/Swagger documentation generation
  • Some UI backoffice ?
  • Graphql like PageInfo object after, before, first, last, pageof

License

MIT License - see LICENSE file for details

Acknowledgments

Inspired by:

Directories ΒΆ

Path Synopsis
orm
The package is the layer that interacts with the database.
The package is the layer that interacts with the database.
This package contains the authorization functions that are used to check if a user is allowed to access a resource
This package contains the authorization functions that are used to check if a user is allowed to access a resource
the serializer package is responsible for serializing and deserializing objects its main purpose is to provide a way to exclude fields from serialization or deserialization allowing restman to generate DTO using only tags on the struct
the serializer package is responsible for serializing and deserializing objects its main purpose is to provide a way to exclude fields from serialization or deserialization allowing restman to generate DTO using only tags on the struct

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL