API Resources

API Resources provide a transformation layer between your models and the JSON responses sent to clients. They let you control exactly which fields are exposed, add computed properties, and format nested relationships — without modifying your models.

The Resource Interface

import "github.com/CodeSyncr/nimbus/resource"

// Resource transforms a model to an API response map.
type Resource interface {
    ToJSON() map[string]any
}

Creating a Resource

package resources

import (
    "github.com/CodeSyncr/nimbus/resource"
    "app/models"
)

type UserResource struct {
    User *models.User
}

func (r *UserResource) ToJSON() map[string]any {
    return map[string]any{
        "id":         r.User.ID,
        "name":       r.User.Name,
        "email":      r.User.Email,
        "avatar_url": r.User.AvatarURL(),
        "created_at": r.User.CreatedAt.Format("2006-01-02"),
    }
    // Note: Password, tokens, internal fields are NOT exposed
}

Using in Controllers

// Single resource
func ShowUser(c *http.Context) error {
    var user models.User
    database.Get().First(&user, c.Param("id"))

    r := &resources.UserResource{User: &user}
    return c.JSON(200, r.ToJSON())
}

// Response:
// { "id": 1, "name": "Jane", "email": "jane@example.com", "avatar_url": "...", "created_at": "2024-01-15" }

Resource Collections

Use resource.Collection() to transform a slice of resources:

func ListUsers(c *http.Context) error {
    var users []models.User
    database.Get().Find(&users)

    // Convert to resource slice
    resources := make([]resource.Resource, len(users))
    for i := range users {
        resources[i] = &UserResource{User: &users[i]}
    }

    return c.JSON(200, resource.Collection(resources))
}

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

ResourceFunc (Inline Resources)

For quick, one-off transformations without defining a struct:

r := resource.ResourceFunc(func() map[string]any {
    return map[string]any{
        "id":    post.ID,
        "title": post.Title,
        "slug":  post.Slug,
    }
})

return c.JSON(200, r.ToJSON())

Nested Resources

type PostResource struct {
    Post *models.Post
}

func (r *PostResource) ToJSON() map[string]any {
    data := map[string]any{
        "id":         r.Post.ID,
        "title":      r.Post.Title,
        "content":    r.Post.Content,
        "created_at": r.Post.CreatedAt,
    }

    // Nest the author resource
    if r.Post.User.ID != 0 {
        data["author"] = (&UserResource{User: &r.Post.User}).ToJSON()
    }

    // Nest comment resources
    if len(r.Post.Comments) > 0 {
        comments := make([]map[string]any, len(r.Post.Comments))
        for i, c := range r.Post.Comments {
            comments[i] = (&CommentResource{Comment: &c}).ToJSON()
        }
        data["comments"] = comments
    }

    return data
}

With Pagination

func ListPosts(c *http.Context) error {
    page, _ := strconv.Atoi(c.Query("page", "1"))
    var posts []models.Post

    paginator := database.Paginate(database.Get(), &posts, page, 15)

    resources := make([]resource.Resource, len(posts))
    for i := range posts {
        resources[i] = &PostResource{Post: &posts[i]}
    }

    return c.JSON(200, map[string]any{
        "data": resource.Collection(resources),
        "meta": paginator.Meta(),
    })
}

Resources vs Serialize

FeatureAPI Resourcesdatabase.Serialize
Computed fields✅ Full control❌ Model fields only
Nested relations✅ Recursive❌ Flat only
Field selection✅ Explicit in code✅ Pick / Omit options
Use casePublic APIs, complex formatsQuick field filtering