Creating a Full-Stack App Using Next.js, Prisma, and PostgreSQL

Table of Contents

  1. Introduction
  2. Setting Up the Development Environment
  3. Creating the PostgreSQL Database
  4. Setting Up Prisma
  5. Creating Models and Migrations with Prisma
  6. Building the API Layer with Next.js API Routes
  7. Creating the Frontend with React and Next.js
  8. Connecting the Frontend with the Backend
  9. Handling Authentication with JWT
  10. Deploying the Full-Stack App
  11. Conclusion

1. Introduction

Building a full-stack application can be a rewarding and challenging experience. In this module, we’ll walk you through creating a complete full-stack app using Next.js, Prisma, and PostgreSQL. We will build a simple application that includes a backend for handling data, a frontend to display and interact with the data, and user authentication with JWT.

This app will allow you to perform CRUD operations (Create, Read, Update, Delete) on a database, and it will also cover authentication and secure handling of user data.


2. Setting Up the Development Environment

To get started, you will need the following tools installed on your machine:

  • Node.js (LTS version recommended)
  • PostgreSQL (local installation or use a cloud provider like Heroku or ElephantSQL)
  • Prisma CLI (to interact with the database)
  • Next.js (for the frontend and backend)

Installation of Dependencies:

  1. Install Next.js:
bashCopyEditnpx create-next-app@latest full-stack-app
cd full-stack-app
  1. Install Prisma and PostgreSQL:
bashCopyEditnpm install prisma @prisma/client
npm install pg
  1. Initialize Prisma:
bashCopyEditnpx prisma init

This will generate the prisma folder and a schema.prisma file.


3. Creating the PostgreSQL Database

If you’re using a local PostgreSQL instance, ensure PostgreSQL is running. If you’re using a hosted database, create a new PostgreSQL instance.

  • Local Setup: If you’re using PostgreSQL locally, you can create a new database using the psql command or a GUI tool like pgAdmin.
bashCopyEditCREATE DATABASE full_stack_app;
  • Heroku Setup: If you’re using Heroku, you can provision a PostgreSQL database via the Heroku dashboard or the CLI.
bashCopyEditheroku addons:create heroku-postgresql:hobby-dev

Once your database is set up, copy the connection URL to use in Prisma’s configuration.


4. Setting Up Prisma

Prisma is an ORM (Object Relational Mapping) tool that simplifies working with databases. We will configure Prisma to connect to the PostgreSQL database and create models.

Configure Database URL in .env File:

Edit the .env file generated by Prisma and add your PostgreSQL connection string.

envCopyEditDATABASE_URL="postgresql://user:password@localhost:5432/full_stack_app?schema=public"

Initialize Prisma with Schema:

In the schema.prisma file, define the database schema. For this tutorial, we’ll create a simple User model.

prismaCopyEdit// prisma/schema.prisma

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

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

model User {
  id        Int     @id @default(autoincrement())
  username  String  @unique
  email     String  @unique
  password  String
  createdAt DateTime @default(now())
}

Run Prisma migration to create the database schema:

bashCopyEditnpx prisma migrate dev --name init

5. Creating Models and Migrations with Prisma

Now that we’ve defined the User model, you can run the Prisma migration to create the necessary tables in your database.

bashCopyEditnpx prisma migrate dev --name create-user-table

You can verify the table is created by accessing the PostgreSQL database or using Prisma Studio:

bashCopyEditnpx prisma studio

6. Building the API Layer with Next.js API Routes

Next.js provides API routes, allowing you to create a backend directly within your Next.js app. API routes are stored in the pages/api directory.

We’ll create an API route to handle user registration.

Create Registration API Route:

tsxCopyEdit// pages/api/register.ts

import { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcryptjs";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === "POST") {
    const { username, email, password } = req.body;

    // Hash the password
    const hashedPassword = await bcrypt.hash(password, 10);

    // Create a new user in the database
    try {
      const user = await prisma.user.create({
        data: {
          username,
          email,
          password: hashedPassword,
        },
      });

      res.status(201).json(user);
    } catch (error) {
      res.status(400).json({ error: "Error creating user" });
    }
  } else {
    res.status(405).json({ error: "Method Not Allowed" });
  }
}

In this route:

  • We accept the username, email, and password in the POST request.
  • The password is hashed using bcryptjs before storing it in the database.
  • If the user is created successfully, we return the user data as a JSON response.

7. Creating the Frontend with React and Next.js

The frontend will allow users to register and view the list of registered users.

Create the Registration Form:

tsxCopyEdit// pages/register.tsx

import { useState } from "react";
import axios from "axios";

const Register = () => {
  const [username, setUsername] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await axios.post("/api/register", { username, email, password });
      alert("Registration successful!");
    } catch (error) {
      setError("An error occurred. Please try again.");
    }
  };

  return (
    <div>
      <h1>Register</h1>
      {error && <p>{error}</p>}
      <form onSubmit={handleSubmit}>
        <label>
          Username:
          <input
            type="text"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
            required
          />
        </label>
        <label>
          Email:
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
          />
        </label>
        <label>
          Password:
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
          />
        </label>
        <button type="submit">Register</button>
      </form>
    </div>
  );
};

export default Register;

In this form:

  • The user enters their username, email, and password.
  • Upon form submission, an API request is made to the /api/register route to create a new user.

8. Connecting the Frontend with the Backend

With the frontend and backend set up, users can register by sending a POST request to the /api/register API route.

Make sure that the frontend and backend are connected and data is being correctly passed between them.


9. Handling Authentication with JWT

For user authentication, we can use JWT (JSON Web Tokens). After successful login or registration, we’ll issue a JWT to the user, which can be used for subsequent requests to access protected routes.

Create an Auth API Route:

tsxCopyEdit// pages/api/login.ts

import { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === "POST") {
    const { email, password } = req.body;

    // Check if user exists
    const user = await prisma.user.findUnique({ where: { email } });
    if (!user) {
      return res.status(401).json({ error: "Invalid credentials" });
    }

    // Verify password
    const isValid = await bcrypt.compare(password, user.password);
    if (!isValid) {
      return res.status(401).json({ error: "Invalid credentials" });
    }

    // Create JWT
    const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET!, { expiresIn: "1h" });

    res.status(200).json({ token });
  } else {
    res.status(405).json({ error: "Method Not Allowed" });
  }
}

10. Deploying the Full-Stack App

Once the app is ready, you can deploy it to services like Vercel, Netlify, or Heroku. These platforms support Next.js and PostgreSQL apps with minimal configuration.

  • For Vercel and Netlify, deploy your app by connecting your GitHub repository to the platform and configuring environment variables for your database and JWT secret.

11. Conclusion

In this module, we built a full-stack application using Next.js, Prisma, and PostgreSQL. We set up the backend with Next.js API routes, connected to a PostgreSQL database using Prisma, and built a simple user registration system. We also added authentication with JWT and deployed the application to production.

This approach to building full-stack apps with Next.js and Prisma makes it easier to manage your database, handle API requests, and build scalable applications with minimal setup.