API Routes
Phase 4: API Routes
Overview
Create the API endpoints for sending verification emails and validating tokens. Includes rate limiting and security considerations.
Requirements
Functional:
- β’POST
/api/auth/send-verification- Send/resend verification email - β’POST
/api/auth/verify-email- Validate token and verify user - β’GET
/api/auth/verification-status- Check if user is verified
Non-functional:
- β’Rate limit resend endpoint
- β’No information leakage
- β’Proper error codes
Implementation
Send Verification Endpoint
// src/app/api/auth/send-verification/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { verificationService } from '@/lib/verification'
import { sendVerificationEmail } from '@/lib/email'
import { getServerSession } from '@/lib/auth'
import { rateLimit } from '@/lib/rate-limit'
const limiter = rateLimit({
interval: 5 * 60 * 1000, // 5 minutes
uniqueTokenPerInterval: 500,
})
export async function POST(req: NextRequest) {
try {
// Rate limit by IP
const ip = req.headers.get('x-forwarded-for') ?? 'anonymous'
const { success: rateLimitOk } = await limiter.check(5, ip)
if (!rateLimitOk) {
return NextResponse.json(
{ error: 'Too many requests. Please try again later.' },
{ status: 429 }
)
}
// Get authenticated user
const session = await getServerSession()
if (!session?.user) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const { id: userId, email, name } = session.user
// Check if already verified
const isVerified = await verificationService.isUserVerified(userId)
if (isVerified) {
return NextResponse.json(
{ error: 'Email already verified' },
{ status: 400 }
)
}
// Create token and send email
const token = await verificationService.createToken({ userId })
const { success, error } = await sendVerificationEmail(email, token, name)
if (!success) {
console.error('Failed to send verification email:', error)
return NextResponse.json(
{ error: 'Failed to send verification email' },
{ status: 500 }
)
}
return NextResponse.json({
success: true,
message: 'Verification email sent'
})
} catch (error) {
console.error('Send verification error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
Verify Email Endpoint
// src/app/api/auth/verify-email/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { verificationService } from '@/lib/verification'
import { z } from 'zod'
const verifySchema = z.object({
token: z.string().length(64, 'Invalid token format'),
})
export async function POST(req: NextRequest) {
try {
const body = await req.json()
// Validate input
const parsed = verifySchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: 'Invalid token format' },
{ status: 400 }
)
}
const { token } = parsed.data
// Verify the token
const result = await verificationService.verifyEmail(token)
if (!result.success) {
return NextResponse.json(
{ error: result.error },
{ status: 400 }
)
}
return NextResponse.json({
success: true,
message: 'Email verified successfully'
})
} catch (error) {
console.error('Verify email error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
Verification Status Endpoint
// src/app/api/auth/verification-status/route.ts
import { NextResponse } from 'next/server'
import { getServerSession } from '@/lib/auth'
import { verificationService } from '@/lib/verification'
export async function GET() {
try {
const session = await getServerSession()
if (!session?.user) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const isVerified = await verificationService.isUserVerified(session.user.id)
return NextResponse.json({
verified: isVerified
})
} catch (error) {
console.error('Verification status error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
Rate Limiting Utility
// src/lib/rate-limit.ts
import { LRUCache } from 'lru-cache'
interface RateLimitOptions {
interval: number
uniqueTokenPerInterval: number
}
interface RateLimitResult {
success: boolean
remaining: number
reset: number
}
export function rateLimit(options: RateLimitOptions) {
const cache = new LRUCache<string, number[]>({
max: options.uniqueTokenPerInterval,
ttl: options.interval,
})
return {
async check(limit: number, token: string): Promise<RateLimitResult> {
const now = Date.now()
const windowStart = now - options.interval
const timestamps = cache.get(token) || []
const validTimestamps = timestamps.filter(t => t > windowStart)
if (validTimestamps.length >= limit) {
return {
success: false,
remaining: 0,
reset: validTimestamps[0] + options.interval,
}
}
validTimestamps.push(now)
cache.set(token, validTimestamps)
return {
success: true,
remaining: limit - validTimestamps.length,
reset: now + options.interval,
}
},
}
}
Alternative: Express/Node.js Routes
// routes/auth.ts
import express from 'express'
import { verificationService } from '@/lib/verification'
import { sendVerificationEmail } from '@/lib/email'
import { authMiddleware } from '@/middleware/auth'
import { rateLimitMiddleware } from '@/middleware/rate-limit'
const router = express.Router()
// Send verification email
router.post('/send-verification',
authMiddleware,
rateLimitMiddleware({ windowMs: 5 * 60 * 1000, max: 5 }),
async (req, res) => {
try {
const { id: userId, email, name } = req.user
const isVerified = await verificationService.isUserVerified(userId)
if (isVerified) {
return res.status(400).json({ error: 'Email already verified' })
}
const token = await verificationService.createToken({ userId })
const { success } = await sendVerificationEmail(email, token, name)
if (!success) {
return res.status(500).json({ error: 'Failed to send email' })
}
res.json({ success: true })
} catch (error) {
console.error('Send verification error:', error)
res.status(500).json({ error: 'Internal server error' })
}
}
)
// Verify email
router.post('/verify-email', async (req, res) => {
try {
const { token } = req.body
if (!token || token.length !== 64) {
return res.status(400).json({ error: 'Invalid token' })
}
const result = await verificationService.verifyEmail(token)
if (!result.success) {
return res.status(400).json({ error: result.error })
}
res.json({ success: true })
} catch (error) {
console.error('Verify email error:', error)
res.status(500).json({ error: 'Internal server error' })
}
})
export default router
Error Response Standards
// Standard error responses
const ERROR_RESPONSES = {
INVALID_TOKEN: { status: 400, error: 'Invalid verification link' },
EXPIRED_TOKEN: { status: 400, error: 'Verification link has expired' },
ALREADY_VERIFIED: { status: 400, error: 'Email already verified' },
RATE_LIMITED: { status: 429, error: 'Too many requests. Please try again later.' },
UNAUTHORIZED: { status: 401, error: 'Unauthorized' },
INTERNAL_ERROR: { status: 500, error: 'Something went wrong' },
}
Success Criteria
- β’ Send verification endpoint requires authentication
- β’ Rate limiting prevents abuse (5 requests/5 minutes)
- β’ Invalid tokens return generic error (no info leakage)
- β’ Expired tokens return appropriate error
- β’ Already verified users get informative error
- β’ Successful verification returns 200
Related Files
- β’Create:
src/app/api/auth/send-verification/route.ts - β’Create:
src/app/api/auth/verify-email/route.ts - β’Create:
src/app/api/auth/verification-status/route.ts - β’Create:
src/lib/rate-limit.ts