As usual, before we end this chapter, let's put everything we've learned into practice. This time, we are going to create a SaaS (Software-as-a-Service) platform using Next.js.
A SaaS is a web application hosted on the cloud and accessible via web browsers. Users can pay a reoccurring fee to access the services, whether it is task management, e-learning, streaming, or other use cases.
Why SaaS
SaaS has revolutionized how businesses deliver software services, making it the preferred choice over traditional applications. Unlike conventional software that requires installation and ongoing maintenance, SaaS applications are cloud-based, always up-to-date, and accessible from any device with an internet connection.
Benefits for Customers
- Cost-Effective: No upfront costs. Users subscribe based on their needs, paying only for what they use.
- Easy Access: Available through web browsers without downloads or complex installation.
- Automatic Updates: Users always have the latest features and security patches without any manual effort.
Benefits for Businesses
- Predictable Revenue: Subscription-based pricing ensures a steady and recurring income stream, increasing customer lifetime value (CLV).
- Scalability: Cloud infrastructure allows businesses to expand their user base without significant additional costs.
- Lower Maintenance: SaaS providers handle updates, security, and performance optimizations, allowing businesses to focus on product development.
SaaS allows businesses to achieve long-term growth while providing a seamless experience for their customers.
What we'll build
In this tutorial, we are going to create a SaaS skeleton with all the core features that are required for all SaaS platforms. It will serve as a starting point for building any SaaS product, whether it’s a project management tool, an e-learning platform, or a customer relationship management (CRM) system.
Here are the core features we are going to implement:
- User Authentication: Secure login with NextAuth.js.
- Subscription & Payments: Implement Stripe for payment handling.
- User Dashboard: A protected dashboard for authenticated users.
- Paywall: Restrict access to premium content for paid users.
- Email Notifications: Send emails for user actions (optional).
- Admin Panel: Manage users, payments, and other administrative settings (optional).
Preparing the project
To get started, go to your work directory and run the following command:
1npx create-next-app@latest
You will be prompted with a few options. Choose the appropriate option and initialize a new Next.js project.
1Need to install the following packages:
2create-next-app@15.1.6
3Ok to proceed? (y)
4✔ What is your project named? … <project_name>
5✔ Would you like to use TypeScript? … No / Yes
6✔ Would you like to use ESLint? … No / Yes
7✔ Would you like to use Tailwind CSS? … No / Yes
8✔ Would you like your code inside a `src/` directory? … No / Yes
9✔ Would you like to use App Router? (recommended) … No / Yes
10✔ Would you like to use Turbopack for `next dev`? … No / Yes
11✔ Would you like to customize the import alias (`@/*` by default)? … No / Yes
12Creating a new Next.js app in /Users/erichu/GitHub/thedevspace/course/nextjs/#1 saas/<project_name>.
After the installation process, go into the new project directory, and the following project structure should be created:
1.
2├── README.md
3├── eslint.config.mjs
4├── jsconfig.json
5├── next.config.mjs
6├── package-lock.json
7├── package.json
8├── postcss.config.mjs
9├── public
10├── src
11│ └── app
12│ ├── favicon.ico
13│ ├── globals.css
14│ ├── layout.jsx
15│ └── page.jsx
16└── tailwind.config.mjs
Start the development server with the following command:
1npm run dev
1> saas-demo@0.1.0 dev
2> next dev
3
4 ⚠ Port 3000 is in use, trying 3001 instead.
5 ▲ Next.js 15.1.6
6 - Local: http://localhost:3001
7 - Network: http://10.0.0.22:3001
8
9 ✓ Starting...
10 ✓ Ready in 2.7s
Open your browser and go to http://localhost:3001
, and you should see the following page showing up.
Creating the layout
With our project initialized, it is time to talk about the design. We'll start with the layout.jsx
, which looks like this by default:
1import { Geist, Geist_Mono } from "next/font/google";
2import "./globals.css";
3
4const geistSans = Geist({
5 variable: "--font-geist-sans",
6 subsets: ["latin"],
7});
8
9const geistMono = Geist_Mono({
10 variable: "--font-geist-mono",
11 subsets: ["latin"],
12});
13
14export const metadata = {
15 title: "Create Next App",
16 description: "Generated by create next app",
17};
18
19export default function RootLayout({ children }) {
20 return (
21 <html lang="en">
22 <body
23 className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
24 {children}
25 </body>
26 </html>
27 );
28}
There are a few things we need to do in the root layout, such as importing CSS, specifying the default font, defining default metadata, and so on. Essentially, anything that needs to be shared across all pages, should be included here in the layout.jsx
.
- Importing CSS
For the CSS, there's not much to do here, as we already specified we are using TailwindCSS when initializing the project. All we need to do here is to import the globals.css
file:
1import "./globals.css";
This ensures that our styles are applied throughout the application.
- Embedding fonts
As for the fonts, we are going the use the defaults as well.
1import { Geist, Geist_Mono } from "next/font/google";
2
3const geistSans = Geist({
4 variable: "--font-geist-sans",
5 subsets: ["latin"],
6});
7
8const geistMono = Geist_Mono({
9 variable: "--font-geist-mono",
10 subsets: ["latin"],
11});
We then apply these fonts to the <body>
element:
1export default function RootLayout({ children }) {
2 return (
3 <html lang="en">
4 <body
5 className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
6 {children}
7 </body>
8 </html>
9 );
10}
But if you prefer using other fonts, follow the methods we discussed in the Next.js fonts lesson.
- Default metadata
In the Next.js project, all layouts and pages can optionally export a metadata
object, which will be translated into the SEO information in the <head>
section.
1export const metadata = {
2 title: "SaaS Demo",
3 description: "SaaS demo project created by TheDevSpace.io",
4};
In this example, the metadata will be rendered into:
1<head>
2 <title>SaaS Demo</title>
3 <meta
4 name="description"
5 content="SaaS demo project created by TheDevSpace.io" />
6</head>
And because this metadata
is exported from the root layout.jsx
, it will act as the default. If any of the pages in the project does not have a metadata
defined, this root layout metadata
will be used instead.
Navbar and footer
Lastly, our SaaS project is going to have many different pages, but some components, such as navbar and footers, are going to be shared across all of them. These components are usually placed inside the layout.jsx
.
Before we end this lesson, let's create the navbar and the footer:
1src
2├── app
3│ ├── favicon.ico
4│ ├── globals.css
5│ ├── layout.jsx
6│ └── page.jsx
7├── components
8│ ├── footer.jsx
9│ └── navbar.jsx
10└── libs
We split them into individual components rather than putting everything into the layout.jsx
because in the next a few lessons, we are going to create a user authentication, a payment system, and a role based access system.
These new features will make our navbar much more complex, as we'll need to display different information for different users.
Putting them into independent files makes your code much more manageable as things get more and more complex.
components/navbar.jsx
1export default function Navbar() {
2 return (
3 <nav className="flex items-center justify-between px-6 py-4 bg-white shadow-md">
4 <a href="/" className="text-xl font-bold">
5 MySaaS
6 </a>
7
8 <div className="hidden md:flex gap-6">
9 <a href="/features" className="hover:text-blue-600">
10 Features
11 </a>
12 <a href="/pricing" className="hover:text-blue-600">
13 Pricing
14 </a>
15 <a href="/about" className="hover:text-blue-600">
16 About
17 </a>
18 </div>
19
20 <a
21 href="/signin"
22 className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
23 Get Started
24 </a>
25 </nav>
26 );
27}
components/footer.jsx
1export default function Footer() {
2 return (
3 <footer className="px-6 py-6 bg-gray-900 text-white">
4 <div className="flex flex-col md:flex-row justify-between items-center">
5 <p className="text-sm">
6 © {new Date().getFullYear()} MySaaS. All rights reserved.
7 </p>
8
9 <div className="flex gap-4 mt-4 md:mt-0">
10 <a href="/privacy" className="hover:text-gray-400">
11 Privacy
12 </a>
13 <a href="/terms" className="hover:text-gray-400">
14 Terms
15 </a>
16 <a href="/contact" className="hover:text-gray-400">
17 Contact
18 </a>
19 </div>
20
21 <div className="flex gap-4 mt-4 md:mt-0">
22 <a
23 href="https://twitter.com"
24 target="_blank"
25 rel="noopener noreferrer">
26 <svg
27 className="w-5 h-5 fill-white hover:fill-gray-400"
28 viewBox="0 0 24 24">
29 <path d=". . ." />
30 </svg>
31 </a>
32 </div>
33 </div>
34 </footer>
35 );
36}
And don't forget to import the Navbar
and Footer
back into layout.jsx
.
app/layout.jsx
1import { Geist, Geist_Mono } from "next/font/google";
2import "./globals.css";
3import Navbar from "@/components/navbar";
4import Footer from "@/components/footer";
5
6. . .
7
8export default function RootLayout({ children }) {
9 return (
10 <html lang="en">
11 <body
12 className={`${geistSans.variable} ${geistMono.variable} antialiased text-gray-900 bg-gray-100`}>
13 <Navbar />
14
15 <main>{children}</main>
16
17 <Footer />
18 </body>
19 </html>
20 );
21}