Mark All Notifications as Read API
Overviewβ
The Mark All Notifications as Read API allows authenticated users to mark all their notifications as read in a single request. This endpoint is useful for bulk operations and improving user experience in notification management interfaces.
Endpoint Detailsβ
- URL:
/api/notifications/read-all
- Method:
POST
- Authentication: Required (NextAuth.js session)
- Content-Type:
application/json
Authenticationβ
This endpoint requires user authentication via NextAuth.js session:
import { getSession } from 'next-auth/react'
const session = await getSession()
if (!session) {
// Handle unauthenticated state
}
Request Bodyβ
This endpoint does not require a request body. All notifications for the authenticated user will be marked as read.
Response Formatβ
Success Response (200)β
{
"success": true,
"message": "5 notifications marked as read",
"markedCount": 5
}
Error Responsesβ
Unauthorized (401)β
{
"error": "User not authenticated"
}
Internal Server Error (500)β
{
"error": "Failed to mark all notifications as read. Please try again later."
}
Implementation Examplesβ
React Component with Bulk Actionβ
import { useState } from 'react'
import { useSession } from 'next-auth/react'
interface Notification {
id: string
title: string
message: string
isRead: boolean
createdAt: string
type: 'info' | 'warning' | 'success' | 'error'
}
function NotificationHeader({
notifications,
onMarkAllAsRead
}: {
notifications: Notification[]
onMarkAllAsRead: () => void
}) {
const { data: session } = useSession()
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const unreadCount = notifications.filter(n => !n.isRead).length
const handleMarkAllAsRead = async () => {
if (!session || unreadCount === 0) return
setLoading(true)
setError(null)
try {
const response = await fetch('/api/notifications/read-all', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to mark all as read')
}
const result = await response.json()
console.log(`Marked ${result.markedCount} notifications as read`)
onMarkAllAsRead()
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error occurred')
} finally {
setLoading(false)
}
}
return (
<div className="flex items-center justify-between p-4 border-b">
<div>
<h2 className="text-xl font-semibold">Notifications</h2>
<p className="text-sm text-gray-500">
{unreadCount > 0 ? `${unreadCount} unread` : 'All caught up!'}
</p>
</div>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-3 py-1 rounded text-sm">
{error}
</div>
)}
{unreadCount > 0 && (
<button
onClick={handleMarkAllAsRead}
disabled={loading}
className="px-4 py-2 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
{loading ? 'Marking...' : `Mark All as Read (${unreadCount})`}
</button>
)}
</div>
)
}
Custom Hook for Bulk Operationsβ
import { useState, useCallback } from 'react'
import { useSession } from 'next-auth/react'
export function useNotificationBulkActions() {
const { data: session } = useSession()
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const markAllAsRead = useCallback(async () => {
if (!session) return { success: false, error: 'Not authenticated' }
setLoading(true)
setError(null)
try {
const response = await fetch('/api/notifications/read-all', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Failed to mark all notifications as read')
}
const result = await response.json()
return {
success: true,
markedCount: result.markedCount,
message: result.message
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
setError(errorMessage)
return { success: false, error: errorMessage }
} finally {
setLoading(false)
}
}, [session])
const markSelectedAsRead = useCallback(async (notificationIds: string[]) => {
if (!session || notificationIds.length === 0) return { success: false, error: 'Invalid request' }
setLoading(true)
setError(null)
try {
const results = await Promise.all(
notificationIds.map(id =>
fetch(`/api/notifications/${id}/read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
)
)
const failed = results.filter(r => !r.ok)
if (failed.length > 0) {
throw new Error(`Failed to mark ${failed.length} notifications as read`)
}
return {
success: true,
markedCount: results.length,
message: `${results.length} notifications marked as read`
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
setError(errorMessage)
return { success: false, error: errorMessage }
} finally {
setLoading(false)
}
}, [session])
return {
markAllAsRead,
markSelectedAsRead,
loading,
error,
clearError: () => setError(null)
}
}
Server Action (Next.js 13+)β
'use server'
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export async function markAllNotificationsAsReadAction() {
const session = await auth()
if (!session) {
redirect('/auth/signin')
}
try {
const response = await fetch(`${process.env.NEXTAUTH_URL}/api/notifications/read-all`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cookie': request.headers.get('cookie') || '',
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to mark all notifications as read')
}
const result = await response.json()
// Revalidate the notifications page
revalidatePath('/notifications')
return {
success: true,
markedCount: result.markedCount,
message: result.message
}
} catch (error) {
console.error('Server action error:', error)
throw error
}
}
React Hook Form Integrationβ
import { useForm } from 'react-hook-form'
import { useNotificationBulkActions } from '@/hooks/useNotificationBulkActions'
interface NotificationBulkForm {
selectAll: boolean
selectedIds: string[]
}
function NotificationBulkActions({ notifications }: { notifications: Notification[] }) {
const { register, watch, setValue, handleSubmit } = useForm<NotificationBulkForm>({
defaultValues: {
selectAll: false,
selectedIds: []
}
})
const { markAllAsRead, markSelectedAsRead, loading } = useNotificationBulkActions()
const watchSelectAll = watch('selectAll')
const watchSelectedIds = watch('selectedIds')
const handleSelectAll = (checked: boolean) => {
setValue('selectAll', checked)
if (checked) {
const unreadIds = notifications.filter(n => !n.isRead).map(n => n.id)
setValue('selectedIds', unreadIds)
} else {
setValue('selectedIds', [])
}
}
const onSubmit = async (data: NotificationBulkForm) => {
if (data.selectAll || data.selectedIds.length === notifications.filter(n => !n.isRead).length) {
// Mark all as read if all unread notifications are selected
await markAllAsRead()
} else {
// Mark selected as read
await markSelectedAsRead(data.selectedIds)
}
}
const unreadCount = notifications.filter(n => !n.isRead).length
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={watchSelectAll}
onChange={(e) => handleSelectAll(e.target.checked)}
className="w-4 h-4 text-blue-600"
/>
<span className="text-sm font-medium">
Select All Unread ({unreadCount})
</span>
</label>
<button
type="submit"
disabled={loading || watchSelectedIds.length === 0}
className="px-4 py-2 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
{loading
? 'Processing...'
: watchSelectedIds.length === unreadCount
? 'Mark All as Read'
: `Mark Selected as Read (${watchSelectedIds.length})`
}
</button>
</div>
<div className="space-y-2">
{notifications.filter(n => !n.isRead).map((notification) => (
<label key={notification.id} className="flex items-center space-x-2 p-2 hover:bg-gray-50 rounded">
<input
type="checkbox"
value={notification.id}
{...register('selectedIds')}
className="w-4 h-4 text-blue-600"
/>
<span className="text-sm">{notification.title}</span>
</label>
))}
</div>
</form>
)
}
cURL Exampleβ
curl -X POST https://ring.ck.ua/api/notifications/read-all \
-H "Content-Type: application/json" \
-H "Cookie: next-auth.session-token=your-session-token"
Business Logicβ
The endpoint performs the following operations:
- Authentication Check: Verifies user session and extracts user ID
- Database Query: Finds all unread notifications for the user
- Bulk Update: Marks all found notifications as read with timestamps
- Count Tracking: Counts the number of notifications that were updated
- Response: Returns success confirmation with count
Database Operationsβ
Query Patternβ
// Firestore query to find unread notifications
const unreadNotifications = await db
.collection('notifications')
.where('userId', '==', userId)
.where('isRead', '==', false)
.get()
// Batch update all notifications
const batch = db.batch()
unreadNotifications.docs.forEach(doc => {
batch.update(doc.ref, {
isRead: true,
readAt: new Date(),
updatedAt: new Date()
})
})
await batch.commit()
Database Schema Impactβ
Before Updateβ
[
{
"id": "notification1",
"userId": "user123",
"title": "New message",
"isRead": false,
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
},
{
"id": "notification2",
"userId": "user123",
"title": "Opportunity update",
"isRead": false,
"createdAt": "2024-01-15T11:00:00Z",
"updatedAt": "2024-01-15T11:00:00Z"
}
]
After Bulk Updateβ
[
{
"id": "notification1",
"userId": "user123",
"title": "New message",
"isRead": true,
"readAt": "2024-01-15T14:22:00Z",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T14:22:00Z"
},
{
"id": "notification2",
"userId": "user123",
"title": "Opportunity update",
"isRead": true,
"readAt": "2024-01-15T14:22:00Z",
"createdAt": "2024-01-15T11:00:00Z",
"updatedAt": "2024-01-15T14:22:00Z"
}
]
Security Considerationsβ
- Authentication Required: Only authenticated users can mark notifications as read
- User Isolation: Users can only mark their own notifications as read
- Bulk Operation Limits: No artificial limits, but database batch operations are used for efficiency
- Rate Limiting: Prevents abuse of bulk operations
Performance Notesβ
- Batch Operations: Uses Firestore batch writes for efficient bulk updates
- Query Optimization: Only queries unread notifications to minimize data transfer
- Transaction Safety: Uses atomic operations to ensure data consistency
- Count Optimization: Returns the actual count of updated notifications
Rate Limitingβ
- User-based: 5 requests per minute per authenticated user (due to bulk nature)
- Global: 100 requests per minute across all users
Error Handlingβ
The API implements comprehensive error handling:
- Authentication errors return 401 status
- Database errors return 500 status with user-friendly messages
- Successful operations return count of affected notifications
Real-time Updates Integrationβ
// WebSocket integration for real-time notification updates
import { useEffect } from 'react'
import { useWebSocket } from '@/hooks/useWebSocket'
function NotificationSystem() {
const { socket } = useWebSocket()
const [notifications, setNotifications] = useState<Notification[]>([])
useEffect(() => {
if (!socket) return
// Listen for bulk read events
socket.on('notifications:bulk-read', (data: { userId: string, count: number }) => {
setNotifications(prev =>
prev.map(notification => ({
...notification,
isRead: true,
readAt: new Date().toISOString()
}))
)
})
return () => {
socket.off('notifications:bulk-read')
}
}, [socket])
return (
<div className="notification-system">
{/* Notification components */}
</div>
)
}
Testingβ
Unit Test Exampleβ
import { POST } from '@/app/api/notifications/read-all/route'
import { NextRequest } from 'next/server'
// Mock authentication and services
jest.mock('@/auth')
jest.mock('@/services/notifications/notification-service')
describe('/api/notifications/read-all', () => {
it('should mark all notifications as read successfully', async () => {
// Mock authenticated session
(getServerAuthSession as jest.Mock).mockResolvedValue({
user: { id: 'user123', role: 'member' }
})
// Mock service to return count
(markAllNotificationsAsRead as jest.Mock).mockResolvedValue(3)
const request = new NextRequest('http://localhost:3000/api/notifications/read-all', {
method: 'POST'
})
const response = await POST(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.markedCount).toBe(3)
expect(data.message).toBe('3 notifications marked as read')
})
it('should handle zero notifications gracefully', async () => {
(markAllNotificationsAsRead as jest.Mock).mockResolvedValue(0)
const request = new NextRequest('http://localhost:3000/api/notifications/read-all', {
method: 'POST'
})
const response = await POST(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.markedCount).toBe(0)
expect(data.message).toBe('0 notifications marked as read')
})
})
Integration Testβ
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import NotificationHeader from '@/components/NotificationHeader'
// Mock notifications data
const mockNotifications = [
{ id: '1', title: 'Test 1', isRead: false, message: 'Test message 1', createdAt: '2024-01-15T10:30:00Z', type: 'info' as const },
{ id: '2', title: 'Test 2', isRead: false, message: 'Test message 2', createdAt: '2024-01-15T11:00:00Z', type: 'info' as const },
{ id: '3', title: 'Test 3', isRead: true, message: 'Test message 3', createdAt: '2024-01-15T09:00:00Z', type: 'info' as const }
]
describe('NotificationHeader Integration', () => {
it('should mark all notifications as read when button is clicked', async () => {
const onMarkAllAsRead = jest.fn()
render(
<NotificationHeader
notifications={mockNotifications}
onMarkAllAsRead={onMarkAllAsRead}
/>
)
// Should show 2 unread notifications
expect(screen.getByText('2 unread')).toBeInTheDocument()
// Click mark all as read button
const button = screen.getByText('Mark All as Read (2)')
fireEvent.click(button)
// Should call the callback
await waitFor(() => {
expect(onMarkAllAsRead).toHaveBeenCalled()
})
})
})
Related Endpointsβ
GET /api/notifications
- List user notificationsPOST /api/notifications/[id]/read
- Mark specific notification as readGET /api/notifications/preferences
- Get notification preferencesPUT /api/notifications/preferences
- Update notification preferences