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
| Feature | API Resources | database.Serialize |
|---|---|---|
| Computed fields | ✅ Full control | ❌ Model fields only |
| Nested relations | ✅ Recursive | ❌ Flat only |
| Field selection | ✅ Explicit in code | ✅ Pick / Omit options |
| Use case | Public APIs, complex formats | Quick field filtering |