Get FAQ
Public list of FAQ groups with their published items. Optional locale filter. Group order is admin-set; items inside each group are admin-ordered too. Answers server-side sanitized.
GET /api/v1/public/faq โ ๐ Public ยท Rate limit: 60 req / minute
Returns FAQ groups (categories) with their published items nested inside. Group order is admin-set (FaqCategory.sortOrder ASC); item order inside each group follows FaqItem.sortOrder ASC. Optional ?locale= filter; otherwise returns groups across all locales.
Items are PUBLISHED-only; groups are not filtered. The category-level filter only applies to items (status = PUBLISHED). Categories themselves are returned regardless of any "active" flag โ but a category with zero published items will still appear with items: []. Frontend should hide empty groups if that's the desired UX.
Answer body is sanitized. Each item's answer field passes through sanitizeHtml() โ strips <script>, <style>, <iframe>, inline on* event handlers. Same caveat as the rest of the content module: admin authors are trusted, sanitization is defense-in-depth.
Request
Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
locale | string | โ | Filter by FaqCategory.locale. Omit for all locales. |
No headers required.
Response headers
| Header | Value |
|---|---|
Cache-Control | public, s-maxage=300, stale-while-revalidate=600 |
Response
200 OK โ ArrayApiResponseOf<PublicFaqGroupDto>
{
"success": true,
"data": [
{
"slug": "account",
"name": "Account",
"items": [
{
"question": "How do I reset my password?",
"answer": "<p>Click Forgot Password on the login screen...</p>"
},
{
"question": "How do I change my email?",
"answer": "<p>Go to Settings...</p>"
}
]
},
{
"slug": "payments",
"name": "Payments",
"items": []
}
]
}Group fields
| Field | Type | Notes |
|---|---|---|
slug | string | FaqCategory.slug (use as anchor / nav target) |
name | string | Display name |
items | array | Published items in this group, ordered by FaqItem.sortOrder ASC. Possibly empty. |
Item fields
| Field | Type | Notes |
|---|---|---|
question | string | Plain text question (not sanitized โ admin-typed plain) |
answer | string | Server-side sanitized HTML answer |
Stripped fields
id, categoryId, status (always PUBLISHED here), sortOrder (used for ordering, not exposed), createdAt, updatedAt, locale (used for filter, not exposed) โ all internal-only, excluded.
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
429 | (throttle) | Rate limit exceeded (60 req/min) |
Side effects
prisma.faqCategory.findMany({ where: locale ? { locale } : {}, orderBy: sortOrder asc, include: { items: { where: { status: PUBLISHED }, orderBy: sortOrder asc } } }).- Map each group โ strip internal fields, sanitize each item's
answerviasanitizeHtml(). - Return the array. No mutations.
Code samples
curl https://api.bio.re/api/v1/public/faq
curl 'https://api.bio.re/api/v1/public/faq?locale=tr'type FaqItem = {
question: string;
answer: string;
};
type FaqGroup = {
slug: string;
name: string;
items: FaqItem[];
};
async function getFaq(locale?: string): Promise<FaqGroup[]> {
const url = new URL('https://api.bio.re/api/v1/public/faq');
if (locale) url.searchParams.set('locale', locale);
const res = await fetch(url);
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'FAQ fetch failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useQuery } from '@tanstack/react-query';
export const contentKeys = {
faq: (locale?: string) => ['content', 'faq', locale ?? 'all'] as const,
};
export function useFaq(locale?: string) {
return useQuery({
queryKey: contentKeys.faq(locale),
queryFn: async () => {
const url = new URL('/api/v1/public/faq', window.location.origin);
if (locale) url.searchParams.set('locale', locale);
const res = await fetch(url);
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'FAQ fetch failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as FaqGroup[];
},
staleTime: 5 * 60_000,
});
}Try it
curl -X GET "https://loading/api/v1/public/faq"{
"success": true,
"data": [
{
"slug": "account",
"name": "Account",
"items": [
{
"question": "How do I reset my password?",
"answer": "<p>Click Forgot Password...</p>"
}
]
}
]
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/content/public-content.controller.ts | 379โ394 (listPublishedFaq), 29โ36 (sanitizeHtml) |
| DTO (response item) | apps/api-core/src/modules/content/dto/content-public-response.dto.ts | 302โ311 (PublicFaqGroupDto), 291โ297 (PublicFaqItemDto) |
| Service | apps/api-core/src/modules/content/content.service.ts | 877โ888 (listPublishedFaq) |
| Prisma models | packages/prisma/prisma/schema.prisma | FaqCategory (admin-ordered), FaqItem (filter status = PUBLISHED, admin-ordered) |
Get Help Article (by slug)
Public read of a single help article by slug. PUBLISHED-only. HTML body server-side sanitized. Embedded category. 404 on missing/draft.
Submit Contact Form
Public contact form submission. Captcha-gated (active provider, admin-managed). Captures user-agent + IP server-side. Stores in ContactMessage and fires fire-and-forget admin notification. Hard 3/hour throttle.