DEV Community

ficav
ficav

Posted on

1

Building a Next.js NSFW Text-to-Image AI Generator with React and Sentry

Building a Next.js NSFW Text-to-Image AI Generator with React and Sentry

In the ever-evolving world of AI and web development, creating applications that can generate images from text prompts is a fascinating challenge. In this article, we’ll explore how to build a text-to-image AI NSFW generator using Next.js, React, TypeScript, Tailwind CSS, and Sentry, with tracking via Facebook Pixel. We’ll cover project setup, Tailwind configuration, Sentry initialization, the core text-to-image API integration, NSFW moderation, frontend UI, error handling, performance monitoring, and deployment. Code snippets are provided throughout so you can follow along hands-on.

Introduction

Text-to-image generation has seen rapid progress thanks to powerful models such as OpenAI’s DALL·E series or Stability AI’s Stable Diffusion. By integrating these APIs into a modern web stack, you can give users the creative freedom to describe scenes, characters, or abstract concepts—and see them visualized. At the same time, responsible handling of potentially NSFW content is crucial. In this guide, we’ll show you how to:

  1. Spin up a Next.js + TypeScript starter
  2. Style with Tailwind CSS
  3. Initialize Sentry for error and performance tracking
  4. Call a text-to-image AI API
  5. Run moderation on NSFW prompts and outputs
  6. Add Facebook Pixel for analytics
  7. Build a responsive React UI
  8. Deploy the finished app to Vercel

Let’s get started!

Technologies Used

  • Next.js – Server-side rendering, API routes, and static exports.
  • React – Component-driven UI.
  • TypeScript – Strong typing for reliability.
  • Tailwind CSS – Utility-first styling for rapid layout.
  • Sentry – Error tracking & performance monitoring.
  • Facebook Pixel – User interaction and conversion analytics.
  • OpenAI / Stability AI API – Backend text-to-image endpoint.
  • NSFW Moderation API – Built-in or third-party endpoint to filter content.

Setting Up the Next.js Project

Open your terminal and run:

npx create-next-app@latest --ts nsfw-text-to-image-generator
cd nsfw-text-to-image-generator
Enter fullscreen mode Exit fullscreen mode

Install core dependencies:

npm install react react-dom next @sentry/nextjs tailwindcss postcss autoprefixer
Enter fullscreen mode Exit fullscreen mode

Initialize Tailwind CSS:

npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Configuring Tailwind CSS

Edit tailwind.config.js to include your Next.js pages and components:

module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Add the Tailwind directives to styles/globals.css:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Initializing Sentry

Sentry helps capture exceptions and performance metrics across both server and client.

  1. Run the Sentry init:
   npx @sentry/wizard -i nextjs
Enter fullscreen mode Exit fullscreen mode

This will install @sentry/nextjs and generate sentry.server.config.js and sentry.client.config.js.

  1. Configure your DSN in .env.local:
   NEXT_PUBLIC_SENTRY_DSN=https://yourPublicDsn@sentry.io/1234567
   SENTRY_ORG=your-org
   SENTRY_PROJECT=your-project
Enter fullscreen mode Exit fullscreen mode
  1. Wrap your application in Sentry’s error boundary. In pages/_app.tsx:
   import { AppProps } from 'next/app';
   import { ErrorBoundary } from '@sentry/react';
   import '@/styles/globals.css';

   function MyApp({ Component, pageProps }: AppProps) {
     return (
       <ErrorBoundary fallback={<p>Something went wrong.</p>}>
         <Component {...pageProps} />
       </ErrorBoundary>
     );
   }

   export default MyApp;
Enter fullscreen mode Exit fullscreen mode

Environment Variables

Create .env.local at the project root:

NEXT_PUBLIC_OPENAI_API_KEY=sk-xxx…
NEXT_PUBLIC_SENTRY_DSN=https://…
NEXT_PUBLIC_FACEBOOK_PIXEL_ID=1234567890
MODERATION_API_KEY=mod-yyy…
Enter fullscreen mode Exit fullscreen mode

Restart your dev server after changing env vars.

Building the Text-to-Image API Route

In pages/api/generate.ts, we’ll call the AI image API and run content moderation:

import type { NextApiRequest, NextApiResponse } from 'next';
import Sentry from '@sentry/nextjs';

const OPENAI_API_KEY = process.env.NEXT_PUBLIC_OPENAI_API_KEY!;
const MOD_API_KEY = process.env.MODERATION_API_KEY!;

type Data = { imageUrl?: string; error?: string };

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  Sentry.captureMessage('API route invoked', 'info');
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { prompt } = req.body;
  if (!prompt) {
    return res.status(400).json({ error: 'Prompt is required' });
  }

  // 1. Moderate the prompt
  const modResponse = await fetch('https://api.moderation.example/v1/check', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${MOD_API_KEY}`,
    },
    body: JSON.stringify({ text: prompt }),
  });
  const modResult = await modResponse.json();
  if (modResult.flagged) {
    return res.status(403).json({ error: 'Prompt flagged as NSFW' });
  }

  try {
    // 2. Generate the image
    const aiRes = await fetch('https://api.openai.com/v1/images/generations', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${OPENAI_API_KEY}`,
      },
      body: JSON.stringify({
        prompt,
        n: 1,
        size: '512x512',
      }),
    });
    const aiData = await aiRes.json();
    const imageUrl = aiData.data[0].url;

    // 3. Optionally moderate the resulting image URL
    // ... (similar to above, using an image moderation endpoint)

    res.status(200).json({ imageUrl });
  } catch (err) {
    Sentry.captureException(err);
    res.status(500).json({ error: 'Failed to generate image' });
  }
}
Enter fullscreen mode Exit fullscreen mode

Handling NSFW Content

  • Prompt Moderation: Check the user’s text before sending it to the AI.
  • Image Moderation: After generation, you can run another check on the image (e.g., via a pixel-based NSFW detection API).
  • User Feedback: If flagged, return a friendly error like “Your prompt contained disallowed content.”

Integrating Facebook Pixel

In pages/_document.tsx, insert the Pixel snippet in the <Head>:

import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head>
          {/* Facebook Pixel */}
          <script
            dangerouslySetInnerHTML={{
              __html: `
              !function(f,b,e,v,n,t,s)
              {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
              n.callMethod.apply(n,arguments):n.queue.push(arguments)};
              if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
              n.queue=[];t=b.createElement(e);t.async=!0;
              t.src=v;s=b.getElementsByTagName(e)[0];
              s.parentNode.insertBefore(t,s)}(window, document,'script',
              'https://connect.facebook.net/en_US/fbevents.js');
              fbq('init', '${process.env.NEXT_PUBLIC_FACEBOOK_PIXEL_ID}');
              fbq('track', 'PageView');
            `,
            }}
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
Enter fullscreen mode Exit fullscreen mode

Track custom events when the user generates or downloads an image:

import Router from 'next/router';

export function trackImageGenerated() {
  if (typeof window !== 'undefined' && window.fbq) {
    window.fbq('track', 'GenerateImagе');
  }
}

Router.events.on('routeChangeComplete', () => {
  if (Router.pathname === '/result') {
    trackImageGenerated();
  }
});
Enter fullscreen mode Exit fullscreen mode

Building the Frontend UI

Create a simple form component in components/PromptForm.tsx:

import { useState } from 'react';
import { trackImageGenerated } from '@/lib/facebook';

export default function PromptForm() {
  const [prompt, setPrompt] = useState('');
  const [imageUrl, setImageUrl] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    setImageUrl(null);

    const res = await fetch('/api/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt }),
    });
    const data = await res.json();
    setLoading(false);

    if (data.error) {
      setError(data.error);
    } else if (data.imageUrl) {
      setImageUrl(data.imageUrl);
      trackImageGenerated();
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <textarea
        value={prompt}
        onChange={(e) => setPrompt(e.target.value)}
        placeholder="Describe your image..."
        className="w-full p-3 border rounded"
        rows={4}
      />
      <button
        type="submit"
        disabled={loading}
        className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
      >
        {loading ? 'Generating…' : 'Generate Image'}
      </button>
      {error && <p className="text-red-500">{error}</p>}
      {imageUrl && (
        <div className="mt-4">
          <img src={imageUrl} alt="Generated" className="rounded shadow" />
        </div>
      )}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Include this in pages/index.tsx:

import PromptForm from '@/components/PromptForm';

export default function Home() {
  return (
    <main className="min-h-screen flex items-center justify-center p-6">
      <div className="max-w-lg w-full">
        <h1 className="text-3xl font-bold mb-6">
          NSFW Text-to-Image Generator
        </h1>
        <PromptForm />
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Error Handling & Performance Monitoring

  • All unhandled exceptions in API routes and React components are captured by Sentry.
  • Use Sentry’s Performance tab to inspect slow API calls or React render bottlenecks.
  • In development, set SENTRY_DEBUG=true to see verbose logs.

Deployment to Vercel

  1. Push your code to GitHub.
  2. Import the repo in Vercel and set your environment variables in the Vercel dashboard.
  3. Choose “Next.js” as the framework and click “Deploy.”

Vercel will handle SSR, API routes, and static assets out of the box.

Conclusion

You’ve now built a full-fledged NSFW-aware text-to-image AI generator with Next.js, React, TypeScript, Tailwind CSS, Sentry, and Facebook Pixel tracking. From secure prompt moderation and AI integration to error monitoring and analytics, this architecture provides a solid foundation for any AI-driven creative app. Feel free to extend it—for example, by adding user authentication, credit-based usage, or a gallery of saved images. Happy coding!

ACI image

ACI.dev: The Only MCP Server Your AI Agents Need

ACI.dev’s open-source tool-use platform and Unified MCP Server turns 600+ functions into two simple MCP tools on one server—search and execute. Comes with multi-tenant auth and natural-language permission scopes. 100% open-source under Apache 2.0.

Star our GitHub!

Top comments (0)

Tiger Data image

🐯 🚀 Timescale is now TigerData: Building the Modern PostgreSQL for the Analytical and Agentic Era

We’ve quietly evolved from a time-series database into the modern PostgreSQL for today’s and tomorrow’s computing, built for performance, scale, and the agentic future.

So we’re changing our name: from Timescale to TigerData. Not to change who we are, but to reflect who we’ve become. TigerData is bold, fast, and built to power the next era of software.

Read more

👋 Kindness is contagious

Discover fresh viewpoints in this insightful post, supported by our vibrant DEV Community. Every developer’s experience matters—add your thoughts and help us grow together.

A simple “thank you” can uplift the author and spark new discussions—leave yours below!

On DEV, knowledge-sharing connects us and drives innovation. Found this useful? A quick note of appreciation makes a real impact.

Okay