Mark Notification Read
Flip a single notification's read flag to true and stamp readAt. Ownership-checked. Idempotent — re-marking only refreshes the timestamp.
PATCH /api/v1/notifications/:id/read — 🔑 Bearer · Rate limit: 60 req / minute
Marks a single notification as read. Ownership is enforced via findFirst({ id, userId }); missing or cross-user lookups return 404 not_found (same code — no enumeration leak between "doesn't exist" and "not yours").
Idempotent re-read. Re-marking an already-read notification refreshes the readAt timestamp to the new "read again" moment. The read flag stays true. If you only want to mark unread→read once, gate client-side off the existing read: true value before issuing the call.
Request
Path parameters
| Param | Type | Validation | Notes |
|---|---|---|---|
id | string (UUID) | ParseUUIDPipe | The notification to mark read |
No body.
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — SuccessOnlyResponseDto
{
"success": true
}| Field | Type | Notes |
|---|---|---|
success | boolean | Always true on 200. Re-fetch the list / unread count to see the post-write state, or update local cache optimistically. |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | (validation) | id not a valid UUID |
401 | (guard) | Missing / invalid bearer token |
404 | error.notification.not_found | Notification not found OR belongs to a different user (same code on purpose) |
429 | (throttle) | Rate limit exceeded (60 req/min) |
Side effects
- Ownership-aware lookup —
prisma.notification.findFirst({ where: { id, userId } }). Returnsnullfor missing OR cross-user → thrownot_found. prisma.notification.update({ where: { id }, data: { read: true, readAt: new Date() } }).- Return
{ success: true }.
Code samples
curl -X PATCH https://api.bio.re/api/v1/notifications/n1a2b3c4-d5e6-7890-abcd-ef1234567890/read \
-H "Authorization: Bearer $ACCESS_TOKEN"async function markNotificationRead(accessToken: string, notificationId: string): Promise<void> {
const res = await fetch(`https://api.bio.re/api/v1/notifications/${notificationId}/read`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${accessToken}` },
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Mark read failed'), {
code: json?.error?.code,
});
}
}import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useMarkNotificationRead() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (notificationId: string) => {
const res = await fetch(`/api/v1/notifications/${notificationId}/read`, {
method: 'PATCH',
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Mark read failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
},
// Optimistic UI: flip local state, invalidate on success
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['notifications'] });
},
});
}Try it
Authorization
bearer In: header
Path Parameters
Response Body
application/json
application/json
application/json
curl -X PATCH "https://loading/api/v1/notifications/string/read"{
"success": true
}{
"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"
}
}{
"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 | 90–104 (markRead — direct Prisma, ownership-aware findFirst) |
| DTO (response) | apps/api-core/src/common/dto/common-response.dto.ts | SuccessOnlyResponseDto |
| Prisma model | packages/prisma/prisma/schema.prisma | Notification.read, Notification.readAt |
Unread Notification Count
Lightweight badge counter — total unread notifications for the calling user. Pairs with the inbox list endpoint for navigation badges.
Mark All Notifications Read
Bulk-flip every unread notification for the calling user to read=true. Returns the count actually updated. Scoped to caller — no cross-user leak.