Profanity Filter in JavaScript: Detect Bad Words with a Profanity Detection API
Introduction
If your website or app lets people type anything that other people will read, sooner or later someone will type something you don't want on your platform. Comments, reviews, chat messages, usernames, and profile bios are all open doors for offensive language, and a few bad posts can quickly change how the rest of your community feels about your product.
A profanity filter is the first line of defense. In JavaScript, you can start with a simple word list, but those lists are easy to fool and hard to maintain. A profanity detection API moves that work to a service that keeps its rules up to date and gives you a clear decision for every piece of text. This guide shows how a profanity filter works, where the check should run, and how to call the Greip Profanity Detection API (v2) from Node.js with the API key safely on the server.
According to Microsoft's 2025 Global Online Safety Survey of 14,800 teens, parents, and adults in 15 countries, 33% of respondents said they had experienced hate speech online in the past year.
How a Profanity Filter Works
Most profanity filters fall into two broad approaches, and modern systems usually combine them.
Blocklists
The simplest filter is a list of banned words. You split the text into words, compare each one against the list, and block or mask anything that matches. It is fast and easy to understand, which is why so many JavaScript tutorials start here. The problems show up quickly in production:
- Evasion: users swap letters for numbers or symbols (l33t speak such as "sh1t" or "@ss"), add spaces or dots between letters, or use look-alike Unicode characters.
- False positives: a naive substring match flags innocent words that happen to contain a banned word, the classic "Scunthorpe problem".
- Maintenance: slang changes constantly, and every new language or market needs its own list.
Scoring and machine learning
Scoring-based filters look at the text as a whole instead of matching single words. They classify the content by severity and return a level or a score, so you can treat a mild word differently from a slur or a threat. Machine learning models go further by learning patterns from large amounts of labeled text, which helps with obfuscated spellings and context. You can read more about this in our guide on how machine learning is changing profanity filtering, and in our technical breakdown of l33t speak, emojis, and other filter evasion tricks.
Greip's Profanity Detection API follows the scoring approach. It classifies matches into risk levels, and the highest severity found in the text wins. Instead of a raw list of matched words, it returns a risk level (low, medium, or high) and a recommendation (accept, review, or reject) that your code can act on directly.
Client-Side vs Server-Side Profanity Checks
It is tempting to run the whole check in the browser, but a client-side filter on its own is not a real filter.
- Anyone can bypass it. Users can disable JavaScript, edit the page, or send requests straight to your backend.
- Your API key would be exposed. Any key embedded in front-end code can be copied from the browser's developer tools and used by someone else.
- Lists ship to the user. A local blocklist is downloaded with your bundle, so it is visible and easy to work around.
The safer pattern is to run the real check on your server, right before you save or publish the content. The browser can still call your own backend early, for example when the user pauses typing, to show a friendly warning before they submit. That gives users fast feedback while the final decision stays on the server.
Keep your Greip API key in a server environment variable and never ship it to the browser.
Step-by-Step: Detect Profanity with the Greip API
1. Get an API key
Create a Greip account and copy your API key from the dashboard. Profanity Detection is available on the Standard plan and above, so check the pricing page for the plan that fits your volume. If you just want to try a few phrases first, the free Profanity Detection tool lets you test text in the browser without writing code.
2. Understand the request
API v2 uses a POST request to https://v2.greipapi.com/score/text with a JSON body. The key goes in the Authorization header as a Bearer token. API v2 does not accept the key in the query string or the request body.
The body fields are:
- text (required): the text you want to check.
- mode (optional):
liveby default, ortestto simulate the integration during development. - format (optional):
JSONby default, withXML,CSV, andNewlinealso supported. - source (optional): your own label for analytics or support correlation, such as a product or channel name.
Here is the same request with cURL:
curl -X POST "https://v2.greipapi.com/score/text" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "This is a sample text without profanity!"}'3. Read the response
A successful response looks like this:
{
"success": true,
"data": {
"risk_level": "low",
"recommendation": "accept"
},
"execution_ms": 120
}The risk_level value is low, medium, or high. A clean text returns low.
The recommendation is accept, review, or reject, and it is derived from the risk level. Most integrations can branch on the recommendation alone.
If something goes wrong, for example a missing or invalid key, the API returns success: false with an error code and description:
{
"success": false,
"code": 101,
"type": "invalid_key",
"description": "The API Key is missing or invalid."
}4. Call the API from Node.js (server-side)
The example below uses Express and the built-in fetch available in Node.js 18 and later. The API key is read from an environment variable, so it never reaches the browser.
// server.js (Node.js 18+)
import express from 'express';
const app = express();
app.use(express.json());
const GREIP_API_KEY = process.env.GREIP_API_KEY;
async function checkProfanity(text) {
const response = await fetch('https://v2.greipapi.com/score/text', {
method: 'POST',
headers: {
'Authorization': `Bearer ${GREIP_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ text, source: 'comments' })
});
const result = await response.json();
if (!result.success) {
throw new Error(`Greip error ${result.code}: ${result.description}`);
}
// result.data contains risk_level and recommendation
return result.data;
}
app.post('/api/comments', async (req, res) => {
const { comment } = req.body;
try {
const { risk_level, recommendation } = await checkProfanity(comment);
if (recommendation === 'reject') {
return res.status(422).json({ error: 'Please remove offensive language and try again.' });
}
if (recommendation === 'review') {
await queueForModeration(comment, risk_level); // your own moderation queue
return res.status(202).json({ status: 'pending_review' });
}
await saveComment(comment); // your own storage logic
return res.status(201).json({ status: 'published' });
} catch (err) {
// If the check fails, hold the content for review instead of publishing it
await queueForModeration(comment, 'unknown');
return res.status(202).json({ status: 'pending_review' });
}
});
app.listen(3000);During development, add mode: 'test' to the request body to simulate the integration before you switch to live traffic.
5. Call your backend from the browser (client-side)
The browser only talks to your own endpoint, never to Greip directly:
async function submitComment(comment) {
const response = await fetch('/api/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment })
});
const result = await response.json();
if (response.status === 422) {
showMessage(result.error);
} else if (result.status === 'pending_review') {
showMessage('Thanks! Your comment will appear after a quick review.');
} else {
showMessage('Your comment has been published.');
}
}Handling the Results: Block, Mask, or Queue for Review
A profanity filter is only as good as what you do with its answer. A simple policy that maps each recommendation to an action works well for most platforms.
Block (reject)
When the recommendation is reject, don't publish the text. Return a clear message so the user knows what to change. A short, neutral message ("Please remove offensive language") works better than silently dropping the post, which tends to frustrate legitimate users.
Queue for review (review)
When the recommendation is review, the text is borderline. Hold it in a moderation queue, publish it only to the author until a moderator approves it, or apply rate limits to the account. This keeps human attention focused on the uncertain cases instead of every single message. Our guide on using profanity scoring to triage user-generated content covers this workflow in more detail.
Accept, or mask when you need to
When the recommendation is accept, publish the content normally. Some products, such as kid-focused apps or live chat, prefer masking over blocking, where offending words are replaced with asterisks. The v2 response gives you a risk level and a recommendation rather than a list of matched words, so masking requires your own replacement step. A common approach is to apply a small local word list for masking while using the API decision to choose between publish, review, and reject.
Tips that reduce false positives and abuse
- Check every field that other users can see, including usernames and display names, not just comments.
- Use the
sourcefield to label each surface (comments, chat, reviews) so you can compare results later. - Combine the text decision with other signals. Repeat offenders, brand-new accounts, and suspicious IP addresses deserve stricter handling than long-standing members.
- Publish clear community guidelines and give users a way to report content the filter missed.
FAQ
How do I filter profanity in JavaScript?
You can start with a word list, but it is easy to bypass. A more reliable approach is to send the text from your Node.js server to a profanity detection API, such as Greip's POST /score/text endpoint, and act on the returned recommendation: accept, review, or reject.
Should I run a profanity filter on the client or the server?
Run the real check on the server. Client-side checks can be bypassed and would expose your API key. You can still call your own backend from the browser to show early warnings, as long as the final decision happens server-side.
What does the Greip Profanity Detection API return?
It returns a risk level of low, medium, or high, and a recommendation of accept, review, or reject derived from that risk level. It also returns the processing time in milliseconds.
Can a profanity filter catch l33t speak and misspellings?
Simple blocklists struggle with l33t speak, extra spaces, and look-alike characters. Scoring and machine learning based filters handle these better because they look at the text as a whole instead of exact word matches.
Is there a free way to test profanity detection?
Yes. Greip's free Profanity Detection tool lets you check text in the browser. For production use, the API is available on the Standard plan and above.
Conclusion
Detecting profanity in JavaScript is easy to start and hard to get right with a word list alone. The reliable pattern is to keep the check on your server, call a profanity detection API, and map each result to a clear action: publish, hold for review, or reject. Greip's API v2 keeps that integration small, with one POST request and a response that already tells you what to do. To see how this fits into a wider moderation setup, explore Greip's content moderation service and the full API documentation.
Get started
Start protecting your business today
Our service is trusted by thousands of businesses worldwide.
- 1,000 requests during trial
- Cancel anytime