Next.js Integration
Integrate Vettly moderation into your Next.js applications.
Installation
bash
npm install @nextauralabs/vettly-react @nextauralabs/vettly-sdkEnvironment Variables
Create .env.local:
bash
# Client-side (public)
NEXT_PUBLIC_VETTLY_API_KEY=vettly_xxxxxxxxxxxxx
# Server-side only
VETTLY_API_KEY=vettly_xxxxxxxxxxxxxWARNING
Use NEXT_PUBLIC_ prefix only for client-side components. For API routes, use the non-public variable.
App Router
Client Components
Use React components with 'use client' directive:
tsx
// app/comments/page.tsx
'use client'
import { ModeratedTextarea } from '@nextauralabs/vettly-react'
import '@nextauralabs/vettly-react/styles.css'
export default function CommentsPage() {
return (
<ModeratedTextarea
apiKey={process.env.NEXT_PUBLIC_VETTLY_API_KEY!}
policyId="moderate"
placeholder="Write a comment..."
/>
)
}Server Actions
Check content in server actions:
tsx
// app/posts/actions.ts
'use server'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const client = new ModerationClient({
apiKey: process.env.VETTLY_API_KEY!
})
export async function createPost(formData: FormData) {
const content = formData.get('content') as string
const result = await client.check({
content,
policyId: 'moderate',
contentType: 'text'
})
if (!result.safe) {
return {
error: 'Content contains inappropriate material',
categories: result.categories.filter(c => c.triggered)
}
}
// Save to database
const post = await db.posts.create({ content })
return { success: true, post }
}tsx
// app/posts/page.tsx
'use client'
import { createPost } from './actions'
export default function PostsPage() {
async function handleSubmit(formData: FormData) {
const result = await createPost(formData)
if (result.error) {
alert(result.error)
} else {
alert('Post created!')
}
}
return (
<form action={handleSubmit}>
<textarea name="content" />
<button type="submit">Post</button>
</form>
)
}API Routes
Create moderation API routes:
typescript
// app/api/moderate/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const client = new ModerationClient({
apiKey: process.env.VETTLY_API_KEY!
})
export async function POST(request: NextRequest) {
try {
const { content, policyId } = await request.json()
const result = await client.check({
content,
policyId: policyId || 'moderate',
contentType: 'text'
})
return NextResponse.json(result)
} catch (error) {
return NextResponse.json(
{ error: 'Moderation failed' },
{ status: 500 }
)
}
}Use from client:
tsx
'use client'
async function checkContent(text: string) {
const response = await fetch('/api/moderate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: text,
policyId: 'moderate'
})
})
return response.json()
}Route Handlers with Middleware
typescript
// app/api/comments/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const client = new ModerationClient({
apiKey: process.env.VETTLY_API_KEY!
})
async function moderateRequest(request: NextRequest) {
const body = await request.json()
const result = await client.check({
content: body.content,
policyId: 'moderate',
contentType: 'text'
})
if (result.action === 'block') {
return NextResponse.json(
{
error: 'Content blocked',
categories: result.categories.filter(c => c.triggered)
},
{ status: 403 }
)
}
return null
}
export async function POST(request: NextRequest) {
// Check content
const moderationError = await moderateRequest(request)
if (moderationError) return moderationError
// Save comment
const body = await request.json()
const comment = await db.comments.create(body)
return NextResponse.json(comment)
}Pages Router
Client-Side Components
tsx
// pages/comments.tsx
import { ModeratedTextarea } from '@nextauralabs/vettly-react'
import '@nextauralabs/vettly-react/styles.css'
export default function CommentsPage() {
return (
<ModeratedTextarea
apiKey={process.env.NEXT_PUBLIC_VETTLY_API_KEY!}
policyId="moderate"
/>
)
}API Routes
typescript
// pages/api/moderate.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const client = new ModerationClient({
apiKey: process.env.VETTLY_API_KEY!
})
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
const { content, policyId } = req.body
const result = await client.check({
content,
policyId: policyId || 'moderate',
contentType: 'text',
metadata: {
ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress,
userAgent: req.headers['user-agent']
}
})
res.json(result)
} catch (error) {
res.status(500).json({ error: 'Moderation failed' })
}
}Middleware Pattern
typescript
// lib/moderation.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const client = new ModerationClient({
apiKey: process.env.VETTLY_API_KEY!
})
type Handler = (req: NextApiRequest, res: NextApiResponse) => Promise<void>
export function withModeration(handler: Handler, options: {
policyId: string
field?: string
}) {
return async (req: NextApiRequest, res: NextApiResponse) => {
const content = options.field
? req.body[options.field]
: req.body.content
if (!content) {
return handler(req, res)
}
const result = await client.check({
content,
policyId: options.policyId,
contentType: 'text'
})
if (result.action === 'block') {
return res.status(403).json({
error: 'Content blocked',
categories: result.categories.filter(c => c.triggered)
})
}
return handler(req, res)
}
}Use the middleware:
typescript
// pages/api/comments.ts
import { withModeration } from '@/lib/moderation'
async function handler(req: NextApiRequest, res: NextApiResponse) {
const comment = await db.comments.create(req.body)
res.json(comment)
}
export default withModeration(handler, {
policyId: 'moderate',
field: 'text'
})Image Uploads
Client-Side Upload
tsx
'use client'
import { ModeratedImageUpload } from '@nextauralabs/vettly-react'
import { useState } from 'react'
export default function ImageUploader() {
const [imageUrl, setImageUrl] = useState<string | null>(null)
const handleUpload = async (file: File, result: any) => {
if (!result.safe) {
alert('Image contains inappropriate content')
return
}
// Upload to your API
const formData = new FormData()
formData.append('image', file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
})
const { url } = await response.json()
setImageUrl(url)
}
return (
<ModeratedImageUpload
apiKey={process.env.NEXT_PUBLIC_VETTLY_API_KEY!}
policyId="marketplace"
onUpload={handleUpload}
/>
)
}Server-Side Processing
typescript
// app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
import { put } from '@vercel/blob'
const client = new ModerationClient({
apiKey: process.env.VETTLY_API_KEY!
})
export async function POST(request: NextRequest) {
const formData = await request.formData()
const file = formData.get('image') as File
if (!file) {
return NextResponse.json({ error: 'No file' }, { status: 400 })
}
// Convert to base64
const buffer = await file.arrayBuffer()
const base64 = Buffer.from(buffer).toString('base64')
// Check image
const result = await client.check({
content: base64,
policyId: 'strict',
contentType: 'image'
})
if (!result.safe) {
return NextResponse.json(
{ error: 'Image flagged', categories: result.categories },
{ status: 400 }
)
}
// Upload to storage
const blob = await put(file.name, file, { access: 'public' })
return NextResponse.json({ url: blob.url })
}Form Handling
With React Hook Form
tsx
'use client'
import { useForm } from 'react-hook-form'
import { useModeration } from '@nextauralabs/vettly-react'
export default function PostForm() {
const { register, handleSubmit, watch } = useForm()
const content = watch('content')
const { result, check } = useModeration({
apiKey: process.env.NEXT_PUBLIC_VETTLY_API_KEY!,
policyId: 'moderate'
})
const onSubmit = async (data: any) => {
if (!result.safe) {
alert('Content contains inappropriate material')
return
}
await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(data)
})
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<textarea
{...register('content')}
onChange={(e) => check(e.target.value)}
style={{ borderColor: result.safe ? 'green' : 'red' }}
/>
{result.flagged && <p>Content flagged!</p>}
<button type="submit" disabled={!result.safe}>
Submit
</button>
</form>
)
}Batch Processing
typescript
// app/api/batch-moderate/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const client = new ModerationClient({
apiKey: process.env.VETTLY_API_KEY!
})
export async function POST(request: NextRequest) {
const { items } = await request.json()
const results = await client.batchCheck({
policyId: 'moderate',
items: items.map((item: any) => ({
id: item.id,
content: item.text,
contentType: 'text'
}))
})
return NextResponse.json(results)
}Webhooks
typescript
// app/api/webhooks/vettly/route.ts
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
export async function POST(request: NextRequest) {
const body = await request.text()
const signature = request.headers.get('x-vettly-signature')
// Verify signature
const expectedSignature = crypto
.createHmac('sha256', process.env.VETTLY_WEBHOOK_SECRET!)
.update(body)
.digest('hex')
if (signature !== expectedSignature) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
}
const event = JSON.parse(body)
if (event.type === 'moderation.completed') {
await handleModerationComplete(event.data)
}
return NextResponse.json({ received: true })
}
async function handleModerationComplete(data: any) {
// Update database with moderation result
await db.posts.update(
{ id: data.itemId },
{ moderationStatus: data.safe ? 'approved' : 'rejected' }
)
}Error Handling
typescript
// lib/moderation-client.ts
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const client = new ModerationClient({
apiKey: process.env.VETTLY_API_KEY!,
timeout: 5000
})
export async function checkContent(
content: string,
policyId: string
) {
try {
return await client.check({
content,
policyId,
contentType: 'text'
})
} catch (error) {
console.error('Moderation error:', error)
// Fail open (allow content) in production
if (process.env.NODE_ENV === 'production') {
return {
safe: true,
flagged: false,
action: 'allow' as const,
categories: [],
decisionId: 'error',
provider: 'error',
latency: 0,
cost: 0
}
}
throw error
}
}TypeScript
Full type safety:
typescript
import { ModerationClient } from '@nextauralabs/vettly-sdk'
import type { CheckResponse } from '@nextauralabs/vettly-shared'
const client = new ModerationClient({
apiKey: process.env.VETTLY_API_KEY!
})
export async function POST(request: NextRequest): Promise<NextResponse<CheckResponse>> {
const { content } = await request.json()
const result = await client.check({
content,
policyId: 'moderate',
contentType: 'text'
})
return NextResponse.json(result)
}Best Practices
1. Use Environment Variables
typescript
// ✅ Good
apiKey: process.env.NEXT_PUBLIC_VETTLY_API_KEY
// ❌ Bad
apiKey: 'vettly_hardcoded_key'2. Server-Side Verification
typescript
// Client-side check (can be bypassed)
const result = await client.check(content)
// ✅ Always verify server-side
export async function POST(request: NextRequest) {
const { content } = await request.json()
const result = await serverSideClient.check({
content,
policyId: 'moderate',
contentType: 'text'
})
if (!result.safe) {
return NextResponse.json({ error: 'Blocked' }, { status: 403 })
}
}3. Cache API Responses
typescript
import { unstable_cache } from 'next/cache'
const checkContent = unstable_cache(
async (content: string, policyId: string) => {
return client.check({ content, policyId, contentType: 'text' })
},
['moderation-check'],
{ revalidate: 3600 } // Cache for 1 hour
)4. Error Boundaries
tsx
// app/error.tsx
'use client'
export default function Error({
error,
reset
}: {
error: Error
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
)
}Testing
Mock Client
typescript
// __mocks__/@nextauralabs/vettly-sdk.ts
export class ModerationClient {
async check() {
return {
safe: true,
flagged: false,
action: 'allow' as const,
categories: [],
decisionId: 'mock',
provider: 'mock',
latency: 0,
cost: 0
}
}
}API Route Tests
typescript
import { POST } from '@/app/api/moderate/route'
describe('/api/moderate', () => {
it('blocks inappropriate content', async () => {
const request = new Request('http://localhost/api/moderate', {
method: 'POST',
body: JSON.stringify({ content: 'bad content' })
})
const response = await POST(request as any)
const data = await response.json()
expect(data.safe).toBe(false)
})
})See Also
- TypeScript SDK - Full SDK reference
- React Components - Pre-built components
- Express Integration - Express middleware
- REST API - Direct HTTP API