Express middleware for content moderation. App Store and platform compliance in one middleware.
npm install @vettly/expressExpress middleware for App Store Guideline 1.2 compliance. Every request evaluated, every decision recorded with full audit trail.
Apple requires every iOS app with user-generated content to implement four things. This middleware handles all four at the route level:
| Requirement | Guideline | Express Integration |
|-------------|-----------|---------------------|
| Content filtering | 1.2.1 | moderateContent() middleware |
| User reporting | 1.2.2 | Re-exported SDK client (POST /v1/reports) |
| User blocking | 1.2.3 | Re-exported SDK client (POST /v1/blocks) |
| Audit trail | — | req.moderationResult.decisionId on every request |
``typescript
import { moderateContent } from '@vettly/express'
app.post('/api/comments',
moderateContent({
apiKey: process.env.VETTLY_API_KEY!,
policyId: 'app-store',
field: 'body.content'
}),
async (req, res) => {
const { moderationResult } = req as any
// moderationResult.decisionId — store for audit trail
res.json({ success: true })
}
)
`
Content moderation at the middleware layer means:
- Consistent enforcement - Every route protected by the same policy
- Fail-open safety - Errors don't block legitimate traffic
- Audit trail access - Decision ID attached to every request
- Graduated responses - Handle block, flag, and warn differently
`bash`
npm install @vettly/express @vettly/sdk
`typescript
import express from 'express'
import { moderateContent } from '@vettly/express'
const app = express()
app.use(express.json())
app.post('/api/comments',
moderateContent({
apiKey: process.env.VETTLY_API_KEY!,
policyId: 'community-safe',
field: 'body.content'
}),
async (req, res) => {
// Content passed moderation - save with audit trail
const { moderationResult } = req as any
await db.comments.create({
content: req.body.content,
moderationDecisionId: moderationResult.decisionId,
action: moderationResult.action
})
res.json({ success: true })
}
)
`
---
`typescript
import { moderateContent } from '@vettly/express'
app.post('/api/posts',
moderateContent({
// Required
apiKey: process.env.VETTLY_API_KEY!,
policyId: 'community-safe',
field: 'body.content', // Path to content in request
// Optional: Custom handlers for each action
onBlock: (req, res, result) => {
// Content violates policy - custom response
res.status(403).json({
error: 'Content blocked',
decisionId: result.decisionId,
categories: result.categories.filter(c => c.triggered)
})
},
onFlag: (req, res, result) => {
// Content flagged for review - still allows through
console.log(Flagged content: ${result.decisionId})
},
onWarn: (req, res, result) => {
// Minor concern - user should be notified
res.setHeader('X-Content-Warning', 'true')
}
}),
yourHandler
)
`
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| apiKey | string | Yes | Your Vettly API key |policyId
| | string | No | Policy ID (default: 'moderate') |field
| | string \| function | Yes | Path to content or async extractor function |onBlock
| | function | No | Custom handler for blocked content |onFlag
| | function | No | Custom handler for flagged content |onWarn
| | function | No | Custom handler for warned content |
---
For complex request structures, use a function:
`typescript${title}\n\n${body}\n\nTags: ${tags.join(', ')}
app.post('/api/posts',
moderateContent({
apiKey: process.env.VETTLY_API_KEY!,
policyId: 'social-media',
field: async (req) => {
// Combine multiple fields
const { title, body, tags } = req.body
return `
}
}),
yourHandler
)
---
The moderation result is attached to the request:
`typescript
app.post('/api/comments',
moderateContent({
apiKey: process.env.VETTLY_API_KEY!,
policyId: 'community-safe',
field: 'body.content'
}),
async (req, res) => {
const { moderationResult } = req as any
// Available fields
console.log(moderationResult.decisionId) // UUID for audit trail
console.log(moderationResult.action) // 'allow' | 'warn' | 'flag' | 'block'
console.log(moderationResult.safe) // boolean
console.log(moderationResult.flagged) // boolean
console.log(moderationResult.categories) // Array of { category, score, triggered }
console.log(moderationResult.latency) // Response time in ms
// Store decision ID for compliance
await db.posts.create({
content: req.body.content,
userId: req.user.id,
moderationDecisionId: moderationResult.decisionId,
moderationAction: moderationResult.action
})
res.json({ success: true })
}
)
`
---
Handle each action type differently:
`typescript
app.post('/api/messages',
moderateContent({
apiKey: process.env.VETTLY_API_KEY!,
policyId: 'messaging',
field: 'body.message',
onBlock: (req, res, result) => {
// Hard block - content violates policy
res.status(403).json({
error: 'Message blocked',
reason: 'Content violates community guidelines',
decisionId: result.decisionId
})
},
onFlag: (req, res, result) => {
// Queue for human review but allow message
queueForReview({
decisionId: result.decisionId,
content: req.body.message,
categories: result.categories.filter(c => c.triggered)
})
// Continues to handler
},
onWarn: (req, res, result) => {
// Add warning header but allow
res.setHeader('X-Content-Warning', 'Please be mindful of community guidelines')
// Continues to handler
}
}),
async (req, res) => {
// Message allowed (or was flag/warn)
await sendMessage(req.body.message)
res.json({ sent: true })
}
)
`
---
The middleware fails open by default (errors allow content through):
`typescript
app.post('/api/comments',
moderateContent({
apiKey: process.env.VETTLY_API_KEY!,
policyId: 'community-safe',
field: 'body.content'
}),
async (req, res) => {
const { moderationResult } = req as any
if (!moderationResult) {
// Moderation failed - log but allow through
console.warn('Moderation unavailable, allowing content')
}
// Your logic
res.json({ success: true })
}
)
`
To fail closed (block on errors), handle it explicitly:
`typescript
app.post('/api/comments',
moderateContent({
apiKey: process.env.VETTLY_API_KEY!,
policyId: 'community-safe',
field: 'body.content'
}),
async (req, res) => {
const { moderationResult } = req as any
if (!moderationResult) {
// Fail closed - reject if moderation unavailable
return res.status(503).json({ error: 'Content moderation unavailable' })
}
res.json({ success: true })
}
)
`
---
Moderate multiple fields in the same request:
`typescript
import { ModerationClient } from '@vettly/express'
const client = new ModerationClient({ apiKey: process.env.VETTLY_API_KEY! })
app.post('/api/profiles', async (req, res) => {
const { displayName, bio, website } = req.body
// Check each field
const [nameResult, bioResult] = await Promise.all([
client.check({ content: displayName, policyId: 'usernames' }),
client.check({ content: bio, policyId: 'bios' })
])
if (nameResult.action === 'block' || bioResult.action === 'block') {
return res.status(403).json({
error: 'Profile content blocked',
decisions: {
displayName: nameResult.decisionId,
bio: bioResult.decisionId
}
})
}
// Save profile with decision IDs
await db.profiles.create({
...req.body,
moderationDecisions: {
displayName: nameResult.decisionId,
bio: bioResult.decisionId
}
})
res.json({ success: true })
})
`
---
Apply to all routes matching a pattern:
`typescript
// Moderate all /api/ugc/* routes
app.use('/api/ugc',
moderateContent({
apiKey: process.env.VETTLY_API_KEY!,
policyId: 'user-content',
field: (req) => req.body.content || req.body.text || ''
})
)
// Individual routes inherit moderation
app.post('/api/ugc/comments', saveComment)
app.post('/api/ugc/reviews', saveReview)
app.post('/api/ugc/posts', savePost)
`
---
Full TypeScript support with typed request:
`typescript
import { Request, Response, NextFunction } from 'express'
import { moderateContent } from '@vettly/express'
import type { CheckResponse } from '@vettly/sdk'
interface ModeratedRequest extends Request {
moderationResult?: CheckResponse
}
app.post('/api/comments',
moderateContent({
apiKey: process.env.VETTLY_API_KEY!,
policyId: 'community-safe',
field: 'body.content'
}),
async (req: ModeratedRequest, res: Response) => {
const { moderationResult } = req
if (moderationResult) {
console.log(Decision: ${moderationResult.decisionId})
}
res.json({ success: true })
}
)
`
---
The SDK client is re-exported for convenience:
`typescript
import { ModerationClient, moderateContent } from '@vettly/express'
// Use middleware for route protection
app.post('/comments', moderateContent({ ... }), handler)
// Use client directly for complex flows
const client = new ModerationClient({ apiKey: '...' })
const result = await client.check({ content, policyId })
``
---
1. Sign up at vettly.dev
2. Go to Dashboard > API Keys
3. Create and copy your key
---
- vettly.dev - Sign up
- docs.vettly.dev - Documentation
- @vettly/sdk - Core SDK