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:
Tech Stack
Setting Up the Foundation
1. Initialize the Project
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/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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
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
// __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
.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
Install Vercel CLI
npm i -g vercel
Deploy
vercel --prod
Monitoring and Analytics
Error Tracking
// 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... │ └─────────────────────────────────────┘
More Posts
Getting Started with AI Development
A comprehensive guide to building your first AI application using modern tools and frameworks.
Full Stack Development Best Practices
Essential patterns and practices for building scalable full-stack applications in 2024.