DEV Community

Cover image for Build an AI-Powered Content Generation Platform
CodeWithDhanian
CodeWithDhanian

Posted on

Build an AI-Powered Content Generation Platform

Project Overview

In this comprehensive tutorial, we'll build a full-stack AI-powered content generation platform that allows users to generate text content using OpenAI's GPT models. The application will feature user authentication, a credit system, and a clean UI for generating and managing AI-generated content.

Tech Stack

  • Frontend: Next.js 14 with TypeScript, Tailwind CSS
  • Backend: Next.js API Routes (Node.js)
  • Database: PostgreSQL with Prisma ORM
  • Authentication: Next-Auth with Google OAuth
  • AI Integration: OpenAI GPT-4 API
  • Deployment: Vercel (frontend/API) and Railway (database)
  • Payments: Stripe (for credit purchases)

Project Structure

ai-content-platform/
├── app/
│   ├── api/
│   │   ├── auth/
│   │   ├── generate/
│   │   ├── webhooks/
│   │   └── ...
│   ├── auth/
│   ├── dashboard/
│   ├── generate/
│   ├── layouts/
│   ├── components/
│   └── ...
├── lib/
│   ├── auth.ts
│   ├── db.ts
│   ├── openai.ts
│   └── ...
├── prisma/
│   └── schema.prisma
└── ...
Enter fullscreen mode Exit fullscreen mode

Prerequisites

  • Node.js 18+ installed
  • PostgreSQL database
  • OpenAI API account
  • Google OAuth credentials
  • Stripe account (optional)

Step 1: Setting Up the Project

Create a new Next.js application:

npx create-next-app@latest ai-content-platform --typescript --tailwind --eslint --app
cd ai-content-platform
Enter fullscreen mode Exit fullscreen mode

Install additional dependencies:

npm install prisma @prisma/client next-auth @next-auth/prisma-adapter openai stripe @stripe/stripe-js
npm install -D @types/stripe
Enter fullscreen mode Exit fullscreen mode

Step 2: Database Configuration

Initialize Prisma:

npx prisma init
Enter fullscreen mode Exit fullscreen mode

Update prisma/schema.prisma:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String? @db.Text
  access_token      String? @db.Text
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String? @db.Text
  session_state     String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  credits       Int       @default(10)
  accounts      Account[]
  sessions      Session[]
  generations   Generation[]
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
}

model Generation {
  id        String   @id @default(cuid())
  prompt    String
  content   String
  model     String
  userId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  createdAt DateTime @default(now())
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}
Enter fullscreen mode Exit fullscreen mode

Set up your environment variables in .env.local:

DATABASE_URL="your_postgresql_connection_string"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="your_nextauth_secret"
OPENAI_API_KEY="your_openai_api_key"
GOOGLE_CLIENT_ID="your_google_oauth_client_id"
GOOGLE_CLIENT_SECRET="your_google_oauth_client_secret"
STRIPE_SECRET_KEY="your_stripe_secret_key"
STRIPE_WEBHOOK_SECRET="your_stripe_webhook_secret"
Enter fullscreen mode Exit fullscreen mode

Generate the Prisma client and push the schema:

npx prisma generate
npx prisma db push
Enter fullscreen mode Exit fullscreen mode

Step 3: Authentication Setup

Create lib/auth.ts:

import { NextAuthOptions } from "next-auth";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import GoogleProvider from "next-auth/providers/google";
import { prisma } from "./db";

export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
    }),
  ],
  callbacks: {
    session({ session, user }) {
      if (session.user) {
        session.user.id = user.id;
        session.user.credits = (user as any).credits;
      }
      return session;
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Create lib/db.ts:

import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
Enter fullscreen mode Exit fullscreen mode

Create the API route at app/api/auth/[...nextauth]/route.ts:

import NextAuth from "next-auth";
import { authOptions } from "@/lib/auth";

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };
Enter fullscreen mode Exit fullscreen mode

Step 4: OpenAI Integration

Create lib/openai.ts:

import OpenAI from "openai";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export const generateContent = async (prompt: string, model: string = "gpt-4") => {
  try {
    const completion = await openai.chat.completions.create({
      model,
      messages: [
        {
          role: "system",
          content: "You are a helpful assistant that generates high-quality content.",
        },
        {
          role: "user",
          content: prompt,
        },
      ],
      max_tokens: 1000,
    });

    return completion.choices[0]?.message?.content || "No content generated";
  } catch (error) {
    console.error("OpenAI API error:", error);
    throw new Error("Failed to generate content");
  }
};
Enter fullscreen mode Exit fullscreen mode

Step 5: Create the Generation API

Create app/api/generate/route.ts:

import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { prisma } from "@/lib/db";
import { generateContent } from "@/lib/openai";

export async function POST(req: NextRequest) {
  try {
    const session = await getServerSession(authOptions);

    if (!session || !session.user) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { prompt, model } = await req.json();

    if (!prompt) {
      return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
    }

    // Check user credits
    const user = await prisma.user.findUnique({
      where: { id: session.user.id },
    });

    if (!user || user.credits < 1) {
      return NextResponse.json({ error: "Insufficient credits" }, { status: 402 });
    }

    // Generate content
    const content = await generateContent(prompt, model);

    // Deduct credit and save generation
    await prisma.$transaction([
      prisma.user.update({
        where: { id: user.id },
        data: { credits: { decrement: 1 } },
      }),
      prisma.generation.create({
        data: {
          prompt,
          content,
          model: model || "gpt-4",
          userId: user.id,
        },
      }),
    ]);

    return NextResponse.json({ content });
  } catch (error) {
    console.error("Generation error:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Create the Main Layout

Create app/layouts/RootLayout.tsx:

import { ReactNode } from 'react';
import Navbar from '@/app/components/Navbar';
import { AuthProvider } from '@/app/components/AuthProvider';

interface RootLayoutProps {
  children: ReactNode;
}

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <AuthProvider>
      <div className="min-h-screen bg-gray-50">
        <Navbar />
        <main className="container mx-auto px-4 py-8">
          {children}
        </main>
      </div>
    </AuthProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Create app/components/Navbar.tsx:

'use client';

import { useSession, signIn, signOut } from "next-auth/react";
import Link from "next/link";

export default function Navbar() {
  const { data: session } = useSession();

  return (
    <nav className="bg-white shadow-sm border-b">
      <div className="container mx-auto px-4">
        <div className="flex justify-between items-center h-16">
          <Link href="/" className="text-xl font-bold text-indigo-600">
            AI Content Generator
          </Link>

          <div className="flex items-center space-x-4">
            {session ? (
              <>
                <Link href="/dashboard" className="text-gray-700 hover:text-indigo-600">
                  Dashboard
                </Link>
                <Link href="/generate" className="text-gray-700 hover:text-indigo-600">
                  Generate
                </Link>
                <span className="text-gray-700">
                  Credits: {session.user.credits}
                </span>
                <button
                  onClick={() => signOut()}
                  className="bg-gray-100 hover:bg-gray-200 text-gray-800 px-4 py-2 rounded-md"
                >
                  Sign Out
                </button>
              </>
            ) : (
              <button
                onClick={() => signIn("google")}
                className="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md"
              >
                Sign In
              </button>
            )}
          </div>
        </div>
      </div>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

Create app/components/AuthProvider.tsx:

'use client';

import { SessionProvider } from "next-auth/react";

export function AuthProvider({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>;
}
Enter fullscreen mode Exit fullscreen mode

Update the main app/layout.tsx:

import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import RootLayout from './layouts/RootLayout';
import './globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'AI Content Generator',
  description: 'Generate high-quality content with AI',
};

export default function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <RootLayout>{children}</RootLayout>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Create the Home Page

Create app/page.tsx:

import Link from "next/link";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";

export default async function Home() {
  const session = await getServerSession(authOptions);

  return (
    <div className="text-center">
      <h1 className="text-4xl font-bold text-gray-900 mb-6">
        AI-Powered Content Generation
      </h1>
      <p className="text-xl text-gray-600 mb-8 max-w-2xl mx-auto">
        Generate high-quality content for your blog, social media, or marketing 
        materials using advanced AI models. Save time and create engaging content 
        with just a few clicks.
      </p>

      {session ? (
        <div className="space-y-4">
          <Link
            href="/generate"
            className="inline-block bg-indigo-600 text-white px-6 py-3 rounded-lg text-lg font-medium hover:bg-indigo-700"
          >
            Start Generating Content
          </Link>
          <p className="text-gray-600">
            You have {session.user.credits} credits remaining
          </p>
        </div>
      ) : (
        <div className="space-y-4">
          <Link
            href="/api/auth/signin"
            className="inline-block bg-indigo-600 text-white px-6 py-3 rounded-lg text-lg font-medium hover:bg-indigo-700"
          >
            Get Started - Sign In
          </Link>
          <p className="text-gray-600">
            Sign up now and get 10 free credits to start generating content
          </p>
        </div>
      )}

      <div className="mt-16 grid grid-cols-1 md:grid-cols-3 gap-8">
        <div className="bg-white p-6 rounded-lg shadow-md">
          <h3 className="text-xl font-semibold mb-3">Blog Content</h3>
          <p className="text-gray-600">
            Create engaging blog posts, articles, and long-form content on any topic.
          </p>
        </div>

        <div className="bg-white p-6 rounded-lg shadow-md">
          <h3 className="text-xl font-semibold mb-3">Social Media</h3>
          <p className="text-gray-600">
            Generate captions, posts, and content ideas for all social media platforms.
          </p>
        </div>

        <div className="bg-white p-6 rounded-lg shadow-md">
          <h3 className="text-xl font-semibold mb-3">Marketing Copy</h3>
          <p className="text-gray-600">
            Create compelling product descriptions, ads, and email marketing content.
          </p>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 8: Create the Generation Page

Create app/generate/page.tsx:

'use client';

import { useState } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';

export default function GeneratePage() {
  const [prompt, setPrompt] = useState('');
  const [model, setModel] = useState('gpt-4');
  const [isGenerating, setIsGenerating] = useState(false);
  const [generatedContent, setGeneratedContent] = useState('');
  const [error, setError] = useState('');
  const { data: session, status } = useSession();
  const router = useRouter();

  if (status === 'loading') {
    return <div className="text-center">Loading...</div>;
  }

  if (!session) {
    router.push('/');
    return null;
  }

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsGenerating(true);
    setError('');

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

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || 'Failed to generate content');
      }

      setGeneratedContent(data.content);
    } catch (err: any) {
      setError(err.message);
    } finally {
      setIsGenerating(false);
    }
  };

  return (
    <div className="max-w-4xl mx-auto">
      <h1 className="text-3xl font-bold text-gray-900 mb-6">Generate Content</h1>

      <div className="bg-white p-6 rounded-lg shadow-md mb-6">
        <p className="text-lg mb-4">
          Credits remaining: <span className="font-semibold">{session.user.credits}</span>
        </p>

        <form onSubmit={handleSubmit} className="space-y-4">
          <div>
            <label htmlFor="model" className="block text-sm font-medium text-gray-700 mb-1">
              AI Model
            </label>
            <select
              id="model"
              value={model}
              onChange={(e) => setModel(e.target.value)}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
            >
              <option value="gpt-4">GPT-4</option>
              <option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
            </select>
          </div>

          <div>
            <label htmlFor="prompt" className="block text-sm font-medium text-gray-700 mb-1">
              Your Prompt
            </label>
            <textarea
              id="prompt"
              value={prompt}
              onChange={(e) => setPrompt(e.target.value)}
              rows={4}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
              placeholder="Describe what you want to generate. For example: 'Write a blog post about the benefits of renewable energy'"
              required
            />
          </div>

          <button
            type="submit"
            disabled={isGenerating || session.user.credits < 1}
            className="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
          >
            {isGenerating ? 'Generating...' : `Generate (1 credit)`}
          </button>
        </form>

        {error && (
          <div className="mt-4 p-3 bg-red-100 text-red-700 rounded-md">
            {error}
          </div>
        )}
      </div>

      {generatedContent && (
        <div className="bg-white p-6 rounded-lg shadow-md">
          <h2 className="text-xl font-semibold mb-4">Generated Content</h2>
          <div className="prose max-w-none">
            {generatedContent.split('\n').map((paragraph, index) => (
              <p key={index}>{paragraph}</p>
            ))}
          </div>

          <div className="mt-6 flex space-x-4">
            <button
              onClick={() => navigator.clipboard.writeText(generatedContent)}
              className="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200"
            >
              Copy to Clipboard
            </button>
            <button
              onClick={() => setGeneratedContent('')}
              className="bg-indigo-100 text-indigo-700 px-4 py-2 rounded-md hover:bg-indigo-200"
            >
              Generate Again
            </button>
          </div>
        </div>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 9: Create the Dashboard Page

Create app/dashboard/page.tsx:

import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { prisma } from "@/lib/db";
import Link from "next/link";

export default async function DashboardPage() {
  const session = await getServerSession(authOptions);

  if (!session || !session.user) {
    return (
      <div className="text-center">
        <p>You need to be signed in to view this page.</p>
        <Link href="/api/auth/signin" className="text-indigo-600 hover:text-indigo-800">
          Sign In
        </Link>
      </div>
    );
  }

  const generations = await prisma.generation.findMany({
    where: { userId: session.user.id },
    orderBy: { createdAt: "desc" },
    take: 10,
  });

  return (
    <div className="max-w-6xl mx-auto">
      <h1 className="text-3xl font-bold text-gray-900 mb-6">Dashboard</h1>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
        <div className="bg-white p-6 rounded-lg shadow-md">
          <h2 className="text-xl font-semibold mb-2">Credits Available</h2>
          <p className="text-3xl font-bold text-indigo-600">{session.user.credits}</p>
        </div>

        <div className="bg-white p-6 rounded-lg shadow-md">
          <h2 className="text-xl font-semibold mb-2">Total Generations</h2>
          <p className="text-3xl font-bold text-indigo-600">{generations.length}</p>
        </div>

        <div className="bg-white p-6 rounded-lg shadow-md">
          <h2 className="text-xl font-semibold mb-2">Get More Credits</h2>
          <button className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">
            Purchase Credits
          </button>
        </div>
      </div>

      <div className="bg-white p-6 rounded-lg shadow-md">
        <h2 className="text-xl font-semibold mb-4">Recent Generations</h2>

        {generations.length === 0 ? (
          <p className="text-gray-600">You haven't generated any content yet.</p>
        ) : (
          <div className="space-y-4">
            {generations.map((generation) => (
              <div key={generation.id} className="border-b border-gray-200 pb-4 last:border-0">
                <div className="flex justify-between items-start mb-2">
                  <h3 className="font-medium text-gray-900">
                    {generation.prompt.length > 60 
                      ? `${generation.prompt.substring(0, 60)}...` 
                      : generation.prompt}
                  </h3>
                  <span className="text-sm text-gray-500">
                    {new Date(generation.createdAt).toLocaleDateString()}
                  </span>
                </div>
                <p className="text-gray-600 text-sm">
                  {generation.content.length > 120 
                    ? `${generation.content.substring(0, 120)}...` 
                    : generation.content}
                </p>
                <div className="text-xs text-gray-500 mt-1">
                  Model: {generation.model}
                </div>
              </div>
            ))}
          </div>
        )}

        <div className="mt-6">
          <Link 
            href="/generate"
            className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700"
          >
            Generate New Content
          </Link>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 10: Add Global Styles

Update app/globals.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --foreground-rgb: 0, 0, 0;
  --background-start-rgb: 214, 219, 220;
  --background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
  :root {
    --foreground-rgb: 255, 255, 255;
    --background-start-rgb: 0, 0, 0;
    --background-end-rgb: 0, 0, 0;
  }
}

body {
  color: rgb(var(--foreground-rgb));
  background: linear-gradient(
      to bottom,
      transparent,
      rgb(var(--background-end-rgb))
    )
    rgb(var(--background-start-rgb));
}

.prose p {
  margin-bottom: 1rem;
  line-height: 1.7;
}
Enter fullscreen mode Exit fullscreen mode

Step 11: Deployment

Create next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverComponentsExternalPackages: ['@prisma/client', 'bcrypt'],
  },
}

module.exports = nextConfig
Enter fullscreen mode Exit fullscreen mode

Create vercel.json for deployment configuration:

{
  "version": 2,
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/next"
    }
  ],
  "env": {
    "DATABASE_URL": "@database_url",
    "NEXTAUTH_URL": "@nextauth_url",
    "NEXTAUTH_SECRET": "@nextauth_secret",
    "OPENAI_API_KEY": "@openai_api_key",
    "GOOGLE_CLIENT_ID": "@google_client_id",
    "GOOGLE_CLIENT_SECRET": "@google_client_secret"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 12: Running the Application

  1. Start the development server:
npm run dev
Enter fullscreen mode Exit fullscreen mode
  1. Open http://localhost:3000 in your browser.

Conclusion

You've now built a complete AI-powered content generation platform with user authentication, credit management, and integration with OpenAI's GPT models. This application provides a solid foundation that you can extend with additional features like:

  1. More AI model options
  2. Content templates for different use cases
  3. Team collaboration features
  4. Advanced content editing capabilities
  5. Content analytics and performance tracking

The platform demonstrates modern full-stack development practices with Next.js 14, TypeScript, Prisma, and NextAuth, making it a great addition to your portfolio.

Top comments (0)