BIO.RE
Content

List Blog Posts

Public paginated list of published blog posts. Optional category filter (by slug). Lightweight items (no body, just excerpt). Categories embedded as { name, slug } pairs. CDN 5min/10min SWR.

GET /api/v1/public/blog โ€” ๐ŸŒ Public ยท Rate limit: 60 req / minute

Returns a paginated list of published blog posts. PUBLISHED-only filter server-side. Each item carries the slug + title + excerpt + featured image + publishedAt + embedded categories โ€” no body (use GET /public/blog/:slug for the full content). Optional ?category=<slug> filter.

Lightweight by design. The list intentionally excludes the full HTML body โ€” keeps the payload small for index pages with many posts. Render the excerpt + featured image in the card; lazy-load the full content via GET /public/blog/:slug on click.

Limit clamped at 50. Server enforces min(max(limit, 1), 50). The default is 20.

Request

Query parameters

ParamTypeDefaultValidationNotes
pagenumber1ParseIntPipe, server-clamped to >= 11-based page index
limitnumber20ParseIntPipe, server-clamped to [1, 50]Items per page
categorystringโ€”โ€”Filter by BlogCategory.slug (e.g. product-updates, engineering)

No headers required.

Response headers

HeaderValue
Cache-Controlpublic, s-maxage=300, stale-while-revalidate=600

Response

200 OK โ€” ApiResponseOf<PublicBlogListDataDto>

{
  "success": true,
  "data": {
    "items": [
      {
        "slug": "introducing-biore",
        "title": "Introducing BIO.RE",
        "excerpt": "A short excerpt of the post...",
        "featuredImage": "https://cdn.bio.re/blog/cover.jpg",
        "publishedAt": "2026-04-29T20:00:00.000Z",
        "categories": [
          { "name": "Product Updates", "slug": "product-updates" }
        ]
      }
    ],
    "total": 42,
    "page": 1,
    "limit": 20,
    "totalPages": 3
  }
}

Item fields

FieldTypeNotes
slugstringPass to GET /public/blog/:slug for the body
titlestringDisplay title
excerptstring | nullShort summary for cards
featuredImagestring | nullCover image URL
publishedAtstring (ISO 8601) | nullWhen the post was published
categoriesarray{ name, slug } pairs โ€” categories embedded for filter UI / breadcrumbs

Top-level fields

FieldTypeNotes
itemsarrayUp to limit posts, ordered by publishedAt DESC
totalnumberTotal matching the filter
page / limit / totalPagesnumberEchoed pagination metadata

Errors

HTTPcode / i18nKeyReason
429(throttle)Rate limit exceeded (60 req/min)

Side effects

  1. Clamp page = max(page, 1), limit = min(max(limit, 1), 50).
  2. Build where โ€” status = PUBLISHED, plus optional categories: { some: { category: { slug: categorySlug } } } filter when category provided.
  3. In parallel: blogPost.findMany({ where, orderBy: publishedAt desc, include: { categories: { include: { category: true } } }, skip, take }) + count({ where }).
  4. Map each row โ†’ strip internal fields, project category embed to { name, slug }.
  5. Return paginated envelope. No mutations.

Code samples

# All blog posts
curl 'https://api.bio.re/api/v1/public/blog?page=1&limit=20'

# Filtered by category
curl 'https://api.bio.re/api/v1/public/blog?category=product-updates'
type BlogCategoryEmbed = {
  name: string;
  slug: string;
};

type BlogListItem = {
  slug: string;
  title: string;
  excerpt: string | null;
  featuredImage: string | null;
  publishedAt: string | null;
  categories: BlogCategoryEmbed[];
};

type BlogListData = {
  items: BlogListItem[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
};

async function listBlogPosts(
  filter: { page?: number; limit?: number; category?: string } = {},
): Promise<BlogListData> {
  const url = new URL('https://api.bio.re/api/v1/public/blog');
  if (filter.page) url.searchParams.set('page', String(filter.page));
  if (filter.limit) url.searchParams.set('limit', String(filter.limit));
  if (filter.category) url.searchParams.set('category', filter.category);
  const res = await fetch(url);
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Blog list fetch failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useQuery, keepPreviousData } from '@tanstack/react-query';

export const contentKeys = {
  blogList: (filter: { page?: number; limit?: number; category?: string }) =>
    ['content', 'blog', 'list', filter] as const,
};

export function useBlogList(filter: { page?: number; limit?: number; category?: string } = {}) {
  return useQuery({
    queryKey: contentKeys.blogList(filter),
    queryFn: async () => {
      const url = new URL('/api/v1/public/blog', window.location.origin);
      if (filter.page) url.searchParams.set('page', String(filter.page));
      if (filter.limit) url.searchParams.set('limit', String(filter.limit));
      if (filter.category) url.searchParams.set('category', filter.category);
      const res = await fetch(url);
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Blog list fetch failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as BlogListData;
    },
    placeholderData: keepPreviousData, // smooth pagination
    staleTime: 5 * 60_000,
  });
}

Try it

GET
/api/v1/public/blog

Query Parameters

page?number

Page number (default: 1)

limit?number

Items per page (default: 20, max: 50)

category?string

Filter by category slug

Response Body

application/json

curl -X GET "https://loading/api/v1/public/blog"
{
  "success": true,
  "data": {
    "items": [
      {
        "slug": "introducing-biore",
        "title": "Introducing BIO.RE",
        "excerpt": "A short excerpt of the post...",
        "featuredImage": "https://cdn.bio.re/blog/cover.jpg",
        "publishedAt": "2019-08-24T14:15:22Z",
        "categories": [
          {
            "slug": "product-updates",
            "name": "Product Updates"
          }
        ]
      }
    ],
    "total": 42,
    "page": 1,
    "limit": 20,
    "totalPages": 3
  }
}

Source

SourcePathLines
Controllerapps/api-core/src/modules/content/public-content.controller.ts101โ€“136 (listBlogPosts)
DTO (response)apps/api-core/src/modules/content/dto/content-public-response.dto.ts96โ€“111 (PublicBlogListDataDto), 73โ€“91 (PublicBlogListItemDto), 61โ€“67 (PublicBlogCategoryEmbedDto)
Serviceapps/api-core/src/modules/content/content.service.ts551โ€“572 (listPublishedBlogPosts)
Prisma modelspackages/prisma/prisma/schema.prismaBlogPost (filter status = PUBLISHED), BlogCategory (embedded via join), enum ContentStatus

On this page