Back to Blog
Next.jsSaaSReact

Building a SaaS Application with Next.js

Step-by-step guide to creating a production-ready SaaS application using Next.js and modern tools.

February 28, 2024
15 min read

Building a SaaS Application with Next.js

Next.js has become the go-to framework for building modern web applications. In this comprehensive guide, we'll build a complete SaaS application from scratch, covering everything from authentication to payments.

Project Overview

We'll be building a project management SaaS with the following features:

  • User authentication and authorization
  • Team collaboration tools
  • Real-time updates
  • Payment integration
  • Admin dashboard
  • Tech Stack

  • **Frontend**: Next.js 14 with App Router
  • **Styling**: Tailwind CSS
  • **Database**: PostgreSQL with Prisma
  • **Authentication**: NextAuth.js
  • **Payments**: Stripe
  • **Deployment**: Vercel
  • Setting Up the Foundation

    1. Initialize the Project

    bash

    npx create-next-app@latest my-saas-app

    cd my-saas-app

    npm install prisma @prisma/client next-auth stripe

    2. Database Setup

    Create your Prisma schema:

    prisma

    // prisma/schema.prisma

    generator client {

    provider = "prisma-client-js"

    }

    datasource db {

    provider = "postgresql"

    url = env("DATABASE_URL")

    }

    model User {

    id String @id @default(cuid())

    email String @unique

    name String?

    createdAt DateTime @default(now())

    updatedAt DateTime @updatedAt

    // Relations

    teams TeamMember[]

    projects Project[]

    }

    model Team {

    id String @id @default(cuid())

    name String

    createdAt DateTime @default(now())

    // Relations

    members TeamMember[]

    projects Project[]

    }

    model Project {

    id String @id @default(cuid())

    title String

    description String?

    status String @default("active")

    createdAt DateTime @default(now())

    // Relations

    teamId String

    team Team @relation(fields: [teamId], references: [id])

    ownerId String

    owner User @relation(fields: [ownerId], references: [id])

    tasks Task[]

    }

    Authentication System

    NextAuth.js Configuration

    javascript

    // app/api/auth/[...nextauth]/route.ts

    import NextAuth from 'next-auth'

    import GoogleProvider from 'next-auth/providers/google'

    import { PrismaAdapter } from '@next-auth/prisma-adapter'

    import { prisma } from '@/lib/prisma'

    const handler = NextAuth({

    adapter: PrismaAdapter(prisma),

    providers: [

    GoogleProvider({

    clientId: process.env.GOOGLE_CLIENT_ID!,

    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,

    }),

    ],

    callbacks: {

    session: async ({ session, token }) => {

    if (session?.user) {

    session.user.id = token.sub!

    }

    return session

    },

    },

    })

    export { handler as GET, handler as POST }

    Protected Routes

    javascript

    // middleware.ts

    import { withAuth } from 'next-auth/middleware'

    export default withAuth(

    function middleware(req) {

    // Add any additional middleware logic here

    },

    {

    callbacks: {

    authorized: ({ token }) => !!token,

    },

    }

    )

    export const config = {

    matcher: ['/dashboard/:path*', '/api/protected/:path*']

    }

    Building the Dashboard

    Main Dashboard Layout

    jsx

    // app/dashboard/layout.tsx

    import { Sidebar } from '@/components/sidebar'

    import { Header } from '@/components/header'

    export default function DashboardLayout({

    children,

    }: {

    children: React.ReactNode

    }) {

    return (

    <div className="flex h-screen bg-gray-100">

    <Sidebar />

    <div className="flex-1 flex flex-col overflow-hidden">

    <Header />

    <main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-100">

    <div className="container mx-auto px-6 py-8">

    {children}

    </div>

    </main>

    </div>

    </div>

    )

    }

    Project Management Features

    jsx

    // app/dashboard/projects/page.tsx

    import { getServerSession } from 'next-auth'

    import { prisma } from '@/lib/prisma'

    import { ProjectCard } from '@/components/project-card'

    export default async function ProjectsPage() {

    const session = await getServerSession()

    const projects = await prisma.project.findMany({

    where: {

    ownerId: session?.user?.id,

    },

    include: {

    team: true,

    _count: {

    select: { tasks: true }

    }

    }

    })

    return (

    <div>

    <h1 className="text-3xl font-bold mb-8">Projects</h1>

    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">

    {projects.map((project) => (

    <ProjectCard key={project.id} project={project} />

    ))}

    </div>

    </div>

    )

    }

    Real-time Features

    WebSocket Integration

    javascript

    // lib/websocket.ts

    import { Server } from 'socket.io'

    export function initializeWebSocket(server: any) {

    const io = new Server(server)

    io.on('connection', (socket) => {

    console.log('User connected:', socket.id)

    socket.on('join-project', (projectId) => {

    socket.join(`project-${projectId}`)

    })

    socket.on('task-update', (data) => {

    socket.to(`project-${data.projectId}`).emit('task-updated', data)

    })

    socket.on('disconnect', () => {

    console.log('User disconnected:', socket.id)

    })

    })

    return io

    }

    Payment Integration

    Stripe Setup

    javascript

    // app/api/create-checkout-session/route.ts

    import { NextRequest, NextResponse } from 'next/server'

    import Stripe from 'stripe'

    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

    export async function POST(req: NextRequest) {

    try {

    const { priceId, userId } = await req.json()

    const session = await stripe.checkout.sessions.create({

    mode: 'subscription',

    payment_method_types: ['card'],

    line_items: [

    {

    price: priceId,

    quantity: 1,

    },

    ],

    success_url: `${process.env.NEXTAUTH_URL}/dashboard?success=true`,

    cancel_url: `${process.env.NEXTAUTH_URL}/pricing?canceled=true`,

    client_reference_id: userId,

    })

    return NextResponse.json({ sessionId: session.id })

    } catch (error) {

    return NextResponse.json({ error: 'Error creating checkout session' }, { status: 500 })

    }

    }

    Subscription Management

    jsx

    // components/subscription-manager.tsx

    'use client'

    import { useState } from 'react'

    import { loadStripe } from '@stripe/stripe-js'

    const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!)

    export function SubscriptionManager({ userId }: { userId: string }) {

    const [loading, setLoading] = useState(false)

    const handleSubscribe = async (priceId: string) => {

    setLoading(true)

    try {

    const response = await fetch('/api/create-checkout-session', {

    method: 'POST',

    headers: { 'Content-Type': 'application/json' },

    body: JSON.stringify({ priceId, userId }),

    })

    const { sessionId } = await response.json()

    const stripe = await stripePromise

    await stripe?.redirectToCheckout({ sessionId })

    } catch (error) {

    console.error('Error:', error)

    } finally {

    setLoading(false)

    }

    }

    return (

    <div className="space-y-4">

    <button

    onClick={() => handleSubscribe('price_1234567890')}

    disabled={loading}

    className="bg-blue-500 text-white px-6 py-2 rounded-lg disabled:opacity-50"

    >

    {loading ? 'Processing...' : 'Subscribe to Pro Plan'}

    </button>

    </div>

    )

    }

    Performance Optimization

    Caching Strategy

    javascript

    // lib/cache.ts

    import { unstable_cache } from 'next/cache'

    export const getCachedProjects = unstable_cache(

    async (userId: string) => {

    return await prisma.project.findMany({

    where: { ownerId: userId },

    include: { team: true }

    })

    },

    ['user-projects'],

    { revalidate: 300 } // 5 minutes

    )

    Image Optimization

    jsx

    import Image from 'next/image'

    export function ProjectThumbnail({ src, alt }: { src: string; alt: string }) {

    return (

    <Image

    src={src || "/placeholder.svg"}

    alt={alt}

    width={300}

    height={200}

    className="rounded-lg object-cover"

    priority

    />

    )

    }

    Testing Strategy

    Unit Tests

    javascript

    // __tests__/project.test.ts

    import { createProject } from '@/lib/projects'

    describe('Project Management', () => {

    it('should create a new project', async () => {

    const projectData = {

    title: 'Test Project',

    description: 'A test project',

    teamId: 'team-123',

    ownerId: 'user-123'

    }

    const project = await createProject(projectData)

    expect(project.title).toBe('Test Project')

    expect(project.status).toBe('active')

    })

    })

    Deployment

    Environment Variables

    bash

    .env.local

    DATABASE_URL="postgresql://..."

    NEXTAUTH_SECRET="your-secret"

    NEXTAUTH_URL="http://localhost:3000"

    GOOGLE_CLIENT_ID="your-google-client-id"

    GOOGLE_CLIENT_SECRET="your-google-client-secret"

    STRIPE_SECRET_KEY="sk_test_..."

    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."

    Vercel Deployment

    bash

    Install Vercel CLI

    npm i -g vercel

    Deploy

    vercel --prod

    Monitoring and Analytics

    Error Tracking

    javascript

    // lib/monitoring.ts

    import * as Sentry from '@sentry/nextjs'

    Sentry.init({

    dsn: process.env.SENTRY_DSN,

    environment: process.env.NODE_ENV,

    })

    export function captureError(error: Error, context?: any) {

    Sentry.captureException(error, { extra: context })

    }

    Conclusion

    Building a SaaS application with Next.js provides a solid foundation for scalable, modern web applications. Key takeaways:

    1. **Start with a solid architecture** - Plan your data models and API structure early

    2. **Implement authentication properly** - Use established solutions like NextAuth.js

    3. **Focus on user experience** - Real-time features and responsive design matter

    4. **Plan for scale** - Implement caching, optimization, and monitoring from the start

    5. **Test thoroughly** - Both automated tests and user testing are crucial

    The SaaS landscape is competitive, but with the right technical foundation and focus on solving real problems, you can build successful applications that users love.

    Remember: the technology is just the foundation—focus on solving real problems for your users.

        ┌─────────────────────────────────────┐
        │  Thanks for reading! 📚             │
        │  More content coming soon...        │
        └─────────────────────────────────────┘
    Emil Sabri

    Emil Sabri

    Software Engineer with experience in computer vision, full stack development, and DevOps. Currently working on SaaS solutions and exploring the intersection of AI and web development.

    More Posts

    March 15, 2024
    8 min read

    Getting Started with AI Development

    A comprehensive guide to building your first AI application using modern tools and frameworks.

    AIDevelopment
    March 10, 2024
    12 min read

    Full Stack Development Best Practices

    Essential patterns and practices for building scalable full-stack applications in 2024.

    Full StackBest Practices