Unread Notification Count
Lightweight badge counter — total unread notifications for the calling user. Pairs with the inbox list endpoint for navigation badges.
GET /api/v1/notifications/unread-count — 🔑 Bearer · Rate limit: 60 req / minute
Returns a single integer — the total unread notifications for the calling user (Notification.read = false). Use to drive navigation / app-icon badges.
This is the notification-system unread count, not the message unread count. The two are separate counters on separate tables (Notification vs Message) — render them as separate badges if you surface both. See GET /messages/unread-count for the message-thread counter.
Request
No body, no params.
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — ApiResponseOf<UnreadNotificationCountResponseDto>
{
"success": true,
"data": {
"count": 5
}
}| Field | Type | Notes |
|---|---|---|
count | number | Total Notification rows where userId = caller AND read = false |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
401 | (guard) | Missing / invalid bearer token |
429 | (throttle) | Rate limit exceeded (60 req/min) |
Side effects
- Single
prisma.notification.count({ where: { userId, read: false } }). - Return
{ count }. No mutations.
Polling guidance
- Refresh on app focus + after marking notifications read.
- Don't tight-loop poll — the badge updates rarely (only when a server-side notification fires or the user marks one read).
- A 30s background refresh covers WebSocket-disconnected sessions.
Code samples
curl https://api.bio.re/api/v1/notifications/unread-count \
-H "Authorization: Bearer $ACCESS_TOKEN"async function getUnreadNotificationCount(accessToken: string): Promise<number> {
const res = await fetch('https://api.bio.re/api/v1/notifications/unread-count', {
headers: { Authorization: `Bearer ${accessToken}` },
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Unread count fetch failed'), {
code: json?.error?.code,
});
}
return json.data.count as number;
}import { useQuery } from '@tanstack/react-query';
export const notificationKeys = {
unreadCount: () => ['notifications', 'unread-count'] as const,
};
export function useUnreadNotificationCount() {
return useQuery({
queryKey: notificationKeys.unreadCount(),
queryFn: async () => {
const res = await fetch('/api/v1/notifications/unread-count');
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Unread count fetch failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data.count as number;
},
staleTime: 30_000,
refetchOnWindowFocus: true,
});
}Try it
Authorization
bearer In: header
Response Body
application/json
application/json
curl -X GET "https://loading/api/v1/notifications/unread-count"{
"success": true,
"data": {
"count": 0
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller (inline impl) | apps/api-core/src/modules/notification/user-notification.controller.ts | 80–87 (unreadCount) |
| DTO (response) | apps/api-core/src/modules/notification/dto/notification-client-response.dto.ts | 39–42 (UnreadNotificationCountResponseDto) |
| Prisma model | packages/prisma/prisma/schema.prisma | Notification.read, Notification.userId |
List Notifications
Paginated in-app notification feed for the calling user. Optional read-state filter. Returns the full row including event key, title, body, and free-form data payload.
Mark Notification Read
Flip a single notification's read flag to true and stamp readAt. Ownership-checked. Idempotent — re-marking only refreshes the timestamp.