Search and Filter

Search and Filter

Recipes for querying and filtering content by datatype, field values, and status. The /query/{datatype} endpoint returns content entries matching a datatype name with optional field filters, sorting, and pagination.

For background on content modeling, see content modeling.

Query Parameters Reference

Parameter Type Default Description
sort string server default Field name to sort by. Prefix with - for descending.
limit int 20 Maximum items to return (server max: 100).
offset int 0 Number of items to skip for pagination.
locale string default locale Locale code for internationalized content.
status string published Content status filter (published, draft).

Filter Operators

Field filters are passed as query parameters. The key is the field name, optionally suffixed with a bracket operator.

Operator Syntax Description
eq (default) field=value or field[eq]=value Exact match
ne field[ne]=value Not equal
gt field[gt]=value Greater than
gte field[gte]=value Greater than or equal
lt field[lt]=value Less than
lte field[lte]=value Less than or equal
like field[like]=%pattern% SQL LIKE pattern match
in field[in]=a,b,c Match any of the listed values

Query by Datatype Slug

The primary query use case. Given a datatype name (e.g., blog-post), returns all published content of that type.

curl:

curl "http://localhost:8080/api/v1/query/blog-post" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response:

{
  "data": [
    {
      "content_data_id": "01HXK4N2F8RJZGP6VTQY3MCSW9",
      "datatype_id": "01HXK3M1E6PWZF9C5TNBQ4H7KJ",
      "author_id": "01JMKW8N3QRYZ7T1B5K6F2P4HD",
      "status": "published",
      "date_created": "2026-01-15T10:00:00Z",
      "date_modified": "2026-02-01T14:30:00Z",
      "published_at": "2026-02-01T14:30:00Z",
      "fields": {
        "title": "Getting Started with ModulaCMS",
        "body": "<p>Welcome to the guide...</p>",
        "category": "tutorials",
        "featured_image": "01HXK6R4J9TYEM3A7WBQN5F2PK"
      }
    }
  ],
  "total": 42,
  "limit": 20,
  "offset": 0,
  "datatype": {
    "name": "blog-post",
    "label": "Blog Post"
  }
}

Go SDK:

result, err := client.Query.Query(ctx, "blog-post", nil)
if err != nil {
    // handle error
}

fmt.Printf("Found %d items (showing %d)\n", result.Total, len(result.Data))
for _, item := range result.Data {
    fmt.Printf("  %s: %s\n", item.ContentDataID, item.Fields["title"])
}

TypeScript SDK (read-only):

const result = await client.queryContent('blog-post')

console.log(`Found ${result.total} items`)
for (const item of result.data) {
  console.log(`  ${item.content_data_id}: ${item.fields.title}`)
}

TypeScript SDK (admin):

const result = await admin.query.query('blog-post')

Filter by Field Value

Pass field filters as query parameters. Bare field names use exact match.

curl:

curl "http://localhost:8080/api/v1/query/blog-post?category=news" \
  -H "Authorization: Bearer YOUR_API_KEY"

Go SDK:

result, err := client.Query.Query(ctx, "blog-post", &modula.QueryParams{
    Filters: map[string]string{
        "category": "news",
    },
})

TypeScript SDK (read-only):

const result = await client.queryContent('blog-post', {
  filters: { category: 'news' },
})

TypeScript SDK (admin):

const result = await admin.query.query('blog-post', {
  filters: { category: 'news' },
})

Sort by Date Descending

Prefix the sort field with - for descending order.

curl:

curl "http://localhost:8080/api/v1/query/blog-post?sort=-date_created" \
  -H "Authorization: Bearer YOUR_API_KEY"

Go SDK:

result, err := client.Query.Query(ctx, "blog-post", &modula.QueryParams{
    Sort: "-date_created",
})

TypeScript SDK:

const result = await client.queryContent('blog-post', {
  sort: '-date_created',
})

Paginate Results

Use limit and offset for page-based pagination.

curl:

# Page 1 (items 1-10)
curl "http://localhost:8080/api/v1/query/blog-post?limit=10&offset=0"

# Page 2 (items 11-20)
curl "http://localhost:8080/api/v1/query/blog-post?limit=10&offset=10"

# Page 3 (items 21-30)
curl "http://localhost:8080/api/v1/query/blog-post?limit=10&offset=20"

Go SDK:

pageSize := 10
page := 2 // 1-indexed

result, err := client.Query.Query(ctx, "blog-post", &modula.QueryParams{
    Limit:  pageSize,
    Offset: (page - 1) * pageSize,
})
if err != nil {
    // handle error
}

totalPages := (int(result.Total) + pageSize - 1) / pageSize
fmt.Printf("Page %d of %d (%d total items)\n", page, totalPages, result.Total)

TypeScript SDK:

const pageSize = 10
const page = 2

const result = await client.queryContent('blog-post', {
  limit: pageSize,
  offset: (page - 1) * pageSize,
})

const totalPages = Math.ceil(result.total / result.limit)
console.log(`Page ${page} of ${totalPages} (${result.total} total items)`)

Combine Filters

Pass multiple filter parameters to combine them with AND logic.

curl:

curl "http://localhost:8080/api/v1/query/blog-post?category=news&status=published&sort=-date_created&limit=5" \
  -H "Authorization: Bearer YOUR_API_KEY"

Go SDK:

result, err := client.Query.Query(ctx, "blog-post", &modula.QueryParams{
    Status: "published",
    Sort:   "-date_created",
    Limit:  5,
    Filters: map[string]string{
        "category": "news",
    },
})

TypeScript SDK:

const result = await client.queryContent('blog-post', {
  status: 'published',
  sort: '-date_created',
  limit: 5,
  filters: {
    category: 'news',
  },
})

Full-Text Search with "like" Operator

Use the [like] operator suffix for SQL LIKE pattern matching. % matches any sequence of characters.

curl:

# Title contains "tutorial"
curl "http://localhost:8080/api/v1/query/blog-post?title[like]=%25tutorial%25" \
  -H "Authorization: Bearer YOUR_API_KEY"

Note: %25 is the URL encoding of %.

Go SDK:

result, err := client.Query.Query(ctx, "blog-post", &modula.QueryParams{
    Filters: map[string]string{
        "title[like]": "%tutorial%",
    },
})

TypeScript SDK:

const result = await client.queryContent('blog-post', {
  filters: {
    'title[like]': '%tutorial%',
  },
})

Range Filters

Use [gt], [gte], [lt], [lte] for range queries. Combine two operators on the same field for a range.

curl:

# Price between 10 and 50
curl "http://localhost:8080/api/v1/query/product?price[gte]=10&price[lte]=50" \
  -H "Authorization: Bearer YOUR_API_KEY"

Go SDK:

result, err := client.Query.Query(ctx, "product", &modula.QueryParams{
    Filters: map[string]string{
        "price[gte]": "10",
        "price[lte]": "50",
    },
})

TypeScript SDK:

const result = await client.queryContent('product', {
  filters: {
    'price[gte]': '10',
    'price[lte]': '50',
  },
})

Match Any of Multiple Values

Use the [in] operator with comma-separated values.

curl:

curl "http://localhost:8080/api/v1/query/blog-post?category[in]=news,tutorials,updates" \
  -H "Authorization: Bearer YOUR_API_KEY"

Go SDK:

result, err := client.Query.Query(ctx, "blog-post", &modula.QueryParams{
    Filters: map[string]string{
        "category[in]": "news,tutorials,updates",
    },
})

TypeScript SDK:

const result = await client.queryContent('blog-post', {
  filters: {
    'category[in]': 'news,tutorials,updates',
  },
})

Query with Locale

Filter content by locale for internationalized sites.

curl:

curl "http://localhost:8080/api/v1/query/blog-post?locale=fr&sort=-date_created" \
  -H "Authorization: Bearer YOUR_API_KEY"

Go SDK:

result, err := client.Query.Query(ctx, "blog-post", &modula.QueryParams{
    Locale: "fr",
    Sort:   "-date_created",
})

TypeScript SDK:

const result = await client.queryContent('blog-post', {
  locale: 'fr',
  sort: '-date_created',
})

Query Draft Content

By default, queries return published content only. Pass status=draft to see unpublished content (requires authentication).

curl:

curl "http://localhost:8080/api/v1/query/blog-post?status=draft" \
  -H "Authorization: Bearer YOUR_API_KEY"

Go SDK:

result, err := client.Query.Query(ctx, "blog-post", &modula.QueryParams{
    Status: "draft",
})

TypeScript SDK:

const result = await admin.query.query('blog-post', {
  status: 'draft',
})

Next Steps