Skip to main content

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:

  1. Authentication Check: Verifies user session and extracts user ID
  2. Database Query: Finds all unread notifications for the user
  3. Bulk Update: Marks all found notifications as read with timestamps
  4. Count Tracking: Counts the number of notifications that were updated
  5. 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()
})
})
})