Resources
> Transform domain models into API responses with collections, pagination, and conditional fields.
The resource package is a transformation layer that converts domain
models into serializable maps ready for JSON responses. It keeps the
public shape of your API decoupled from internal struct layout.
Import path: github.com/velocitykode/velocity/resource
The Resource interface
Every resource type implements a single method:
type Resource interface {
ToResource() map[string]any
}Define a resource for each model that leaves your application:
package resources
import "myapp/internal/models"
type UserResource struct {
models.User
}
func (r UserResource) ToResource() map[string]any {
return map[string]any{
"id": r.ID,
"name": r.Name,
"email": r.Email,
// Password deliberately excluded — never leaves the server
}
}Send it from a handler with ctx.Resource:
func Show(ctx *router.Context) error {
user, err := models.FindUser(ctx.Param("id"))
if err != nil {
return err
}
return ctx.Resource(resources.UserResource{User: user})
}ctx.Resource calls ToResource() and writes it as a 200 JSON response.
Collections
NewCollection transforms a typed slice into []map[string]any:
users := []models.User{ /* ... */ }
resources := resource.NewCollection([]resources.UserResource{
{User: users[0]},
{User: users[1]},
})
return ctx.JSON(http.StatusOK, resources)The generic constraint [T Resource] ensures at compile time that every
element has a ToResource() method.
Paginated collections
NewPaginatedCollection wraps items in a {data, meta} envelope:
return ctx.JSON(http.StatusOK, resource.NewPaginatedCollection(
[]resources.UserResource{{User: u1}, {User: u2}},
resource.PaginationMeta{
Total: 42,
PerPage: 15,
CurrentPage: 1,
},
))LastPage is computed automatically (ceiling division) — you only
supply Total, PerPage, and CurrentPage.
Response shape:
{
"data": [
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"}
],
"meta": {
"total": 42,
"per_page": 15,
"current_page": 1,
"last_page": 3
}
}Building meta from an ORM paginator
ORM paginators satisfy the Paginator interface:
type Paginator interface {
Total() int
PerPage() int
CurrentPage() int
Items() any
}Use FromPaginator to convert a paginator into PaginationMeta:
page, err := models.Users().Paginate(ctx, 15)
if err != nil {
return err
}
userResources := make([]resources.UserResource, 0, len(page.Items().([]models.User)))
for _, u := range page.Items().([]models.User) {
userResources = append(userResources, resources.UserResource{User: u})
}
return ctx.JSON(http.StatusOK, resource.NewPaginatedCollection(
userResources,
resource.FromPaginator(page),
))Conditional fields
Four helpers let you include fields only when a condition holds.
When — static condition
func (r UserResource) ToResource() map[string]any {
m := map[string]any{
"id": r.ID,
"name": r.Name,
}
if k, v, ok := resource.When(r.ShowEmail, "email", r.Email); ok {
m[k] = v
}
return m
}WhenNotNil — skip nil values (including typed nils)
if k, v, ok := resource.WhenNotNil("avatar_url", r.AvatarURL); ok {
m[k] = v
}Uses reflection so that (*string)(nil) is correctly detected as nil.
A plain v == nil check misses typed nils wrapped in an interface.
WhenFunc — lazy evaluation
When computing the value is expensive, defer it until the condition is actually true:
if k, v, ok := resource.WhenFunc(r.IncludePosts, "posts", func() any {
return loadPostsForUser(r.ID) // only called when IncludePosts is true
}); ok {
m[k] = v
}Merge — compose conditional blocks
For longer resources, group conditions into closures and apply them in sequence:
func (r UserResource) ToResource() map[string]any {
m := map[string]any{"id": r.ID, "name": r.Name}
resource.Merge(m,
func(m map[string]any) {
if r.ShowEmail {
m["email"] = r.Email
}
},
func(m map[string]any) {
if r.CreatedAt != nil {
m["created_at"] = r.CreatedAt.Format(time.RFC3339)
}
},
)
return m
}Design notes
- Leaf package.
resourceimports nothing from velocity — only the standard library (reflect). It can be consumed by tests and tools without dragging in the rest of the framework. - Decoupling pagination. The
Paginatorinterface lets theormpackage produce meta withoutresourceimportingorm. - No middleware, no side effects. Pure transformation — deterministic and easy to test.