In this article, I’ll walk you through setting up a Next.js + Prisma + Cypress testing environment. My goal is to keep things as simple as possible, so we’ll use SQLite as the database and React with inline styles for the frontend.

Create a New Next.js Project

Run the following command to create a Next.js app:

npx create-next-app@latest

Press Enter on all prompts to accept the default settings. We’ll be using JavaScript (not TypeScript) and won’t include Tailwind CSS.

Once the setup is complete, open the project in your IDE (e.g., VS Code).

Install Prisma

Next, install Prisma as a development dependency:

npm install prisma --save-dev

Then, initialize Prisma with SQLite as the database provider:

npx prisma init --datasource-provider sqlite

Configure the Database

After running the command, a .env file will be created in your project. Open it and you’ll see the database connection string:

DATABASE_URL="file:./dev.db"

This tells Prisma to use a local SQLite database file (dev.db).

Define the Database Schema

Open the prisma/schema.prisma file and replace its contents with the following:

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

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

model Book {
  id     Int     @id @default(autoincrement())
  title  String  @unique
  author String?
}

This defines a simple Book model with:

  • An id that auto-increments.
  • A title field that must be unique.
  • An optional author field.

Run Initial Migration

To apply the schema to your database, run:

npx prisma migrate dev --name init

This will create the database file (dev.db) and apply the initial schema.

Create Server Actions for Books

First, create a new file to handle database interactions:

/lib/actions.js

'use server';

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export const createBook = async (data) => {
  return await prisma.book.create({ data });
};

export const getBooks = async () => {
  return await prisma.book.findMany({});
};

export const deleteBook = async (id) => {
  return await prisma.book.delete({ where: { id } });
};

Clean Up Next.js Default Files

To simplify the project, delete all files inside the /app/ directory except page.js and layout.js.

Create a Simple Books Interface

/app/books.jsx

'use client';

import { createBook, deleteBook } from '@/lib/actions';
import { useRouter } from 'next/navigation';
import { useState } from 'react';

const Book = ({ book, destroy }) => (
  <li>
    <button onClick={destroy}>X</button> &nbsp;
    <b>Title:</b> {book.title}, Author:b> {book.author}
  </li>
);

const Books = ({ books }) => {
  const [title, setTitle] = useState('');
  const [author, setAuthor] = useState('');
  const [error, setError] = useState(false);
  const { refresh } = useRouter();

  const create = async () => {
    try {
      await createBook({ title, author });
      setError(false);
      refresh();
    } catch (e) {
      setError(true);
    }
  };

  const destroy = async (id) => {
    await deleteBook(id);
    refresh();
  };

  return (
    <main>
      <div>
        <input onChange={(e) => setTitle(e.target.value)} name="title" />
        <label> Title </label>
      </div>
      <div>
        <input onChange={(e) => setAuthor(e.target.value)} name="author" />
        <label> Author </label>
      </div>
      {error && <p style={{ color: 'red' }}>Invalid data provided</p>}
      <br />
      <button onClick={create}>Add new book</button>
      <ul>
        {books.map((book) => (
          <Book book={book} destroy={() => destroy(book.id)} key={book.id} />
        ))}
      </ul>
    </main>
  );
};

export default Books;

Connect UI to the Server

/app/page.js

import { getBooks } from '@/lib/actions';
import Books from './books';

const Home = async () => {
  const books = await getBooks();

  return (
    <>
      <h1>Books</h1>
      <Books books={books} />
    </>
  );
};

export default Home;

Update the Root Layout

/app/layout.js

export default function Layout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Running the Application

To start your app, run the following command:

npm run dev

This will launch your Next.js application at http://localhost:3000.

Simple UI

In this simple form, you can easily add and remove books. The model makes sure the title is unique and isn’t left blank. If anything goes wrong, you’ll see an error message.

Installing and Setting Up Cypress

To add Cypress to your project, run the following command:

npm install cypress --save-dev

Next, initialize Cypress and generate the default files and folders by running:

npx cypress open

When the Cypress window appears:

Cypress UI

Select E2E Testing as your default configuration.
Click Continue to confirm the setup.

Once the setup is complete, close both the Next.js development server and the Cypress window for now.

Setting Up the Test Environment

To ensure our tests run safely and efficiently, setting up a dedicated test environment is essential. Using a separate test database helps protect your development data from accidental changes, as tests often create, update, or delete records. This isolation also ensures tests start with a clean state, making them more consistent and reliable.

Step 1: Install dotenv-cli

We'll use dotenv-cli to manage environment variables for our test setup.

Run the following command:

npm install dotenv-cli --save-dev

Step 2: Create a .env.test File

Next, create a new file called .env.test in your project's root directory.

Add the following content to define your test database URL:

DATABASE_URL="file:./test.db"

Since we're using SQLite, this configuration points to a separate database file called test.db — keeping your development data safe.

Creating a Database Cleaner

Since we currently have only one model (Book), we’ll clean just that for now. As more models are added, this cleaner can be expanded.

Create the file /cypress/tasks/clean.js:

const { PrismaClient } = require('@prisma/client');

const prisma = new PrismaClient();

const clean = async () => {
  return await prisma.book.deleteMany({});
};

module.exports = clean;

Since we don’t use TypeScript, we must follow the CommonJS module system (e.g., module.exports) instead of ES module syntax (e.g., export default).

Creating a Test Data Factory

We'll create a simple task for adding test data. In our tests, we'll pass an object to this task to generate the necessary database entities.

Create the file /cypress/tasks/createTestBook.js:

const { PrismaClient } = require('@prisma/client');

const prisma = new PrismaClient();

const createTestBook = async ({
  title = 'Death Note',
  author = 'unknown',
} = {}) => {
  return await prisma.book.create({
    data: { title, author },
  });
};

module.exports = createTestBook;

Adding Tasks to Cypress Configuration

Now, let’s register these tasks in the Cypress config file.

/cypress.config.js:

const { defineConfig } = require("cypress");
const clean = require('./cypress/tasks/clean');
const createTestBook = require('./cypress/tasks/createTestBook');

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3001',
    setupNodeEvents(on, config) {
      on('task', {
        clean,
        createTestBook,
      });
    },
  },
});

Adding Scripts to package.json

We need some helpful scripts to simplify the process of:

  • Running the server on a separate port with the test environment.
  • Running Cypress tests.
  • Applying migrations on the test database.

In your package.json, add the following scripts:

"test:cypress": "dotenv -e .env.test -- cypress open",
"test:server": "dotenv -e .env.test -- next dev -p 3001",
"test:migrate": "dotenv -e .env.test -- npx prisma migrate deploy"

Creating the Test Database

Finally, run the following command to apply your migrations to the test database:

npm run test:migrate

This will create the test.db file, ensuring your test environment is properly configured.

Running the Test Server and Cypress

We need to run the test server and Cypress simultaneously. To do this, open two separate terminals and run the following commands:

npm run test:server

npm run test:cypress

Creating the First Test

Now let's write our first Cypress test.

Create the file /cypress/e2e/books.cy.js and add the following code:

describe('Book', () => {
  beforeEach(() => {
    cy.task('clean')
    cy.task('createTestBook')
    cy.task('createTestBook', { title: 'Memoirs of a Geisha' })
    cy.visit('/')
  })

  it('can be created', () => {
    cy.get('[name="title"]').type('Necronomicon')
    cy.get('[name="author"]').type('Abdul Alhazred')
    cy.contains('Add new book').click()
    cy.contains('Title: Necronomicon').should('exist')
  })
})

What Happens in This Test?

  • The test database is cleaned before each test run.
  • Two books are created — one with the default name and one titled Memoirs of a Geisha.
  • The test visits the homepage.
  • It types a new book's title and author.
  • The new book is added to the database, and the test checks that it’s displayed on the page.

Running the Test

With both the test server and Cypress running, you should see something like this:

Successful run

That’s it — it works!

Adding More Tests

Now let's expand our test suite by adding two more specs — one to test invalid input and another to test book deletion.

Here’s the updated /cypress/e2e/books.cy.js file:

describe('Book', () => {
  beforeEach(() => {
    cy.task('clean')
    cy.task('createTestBook')
    cy.task('createTestBook', { title: 'Memoirs of a Geisha' })
    cy.visit('/')
  })

  it('can be created', () => {
    cy.get('[name="title"]').type('Necronomicon')
    cy.get('[name="author"]').type('Abdul Alhazred')
    cy.contains('Add new book').click()
    cy.contains('Title: Necronomicon').should('exist')
  })

  it('can be deleted', () => {
    cy.contains('X').click()
    cy.contains('Title: Death Note').should('not.exist')
    cy.contains('X').click()
    cy.contains('Title: Memoirs of a Geisha').should('not.exist')
  })

  it('cannot be created with an invalid title', () => {
    cy.get('[name="title"]').type('Death Note')
    cy.contains('Add new book').click()
    cy.contains('Invalid data provided').should('exist')
  })
})

So our tests check the following things:

  • Book Creation: Verifies that a new book can be added successfully.
  • Book Deletion: Ensures books can be removed from the database.
  • Invalid Input Handling: Confirms that attempting to create a book with an existing title shows an error message.

Running the Tests

With both the test server and Cypress running, check the Cypress UI to confirm that all tests pass successfully.

Successful tests run

Wrapping Up

In this article, we’ve built a simple yet effective testing setup using Next.js, Prisma, and Cypress. While this is a basic configuration, it provides a solid foundation that can be expanded to meet your project's specific needs.

Here you can see the repository - https://github.com/AlekseevArthur/next-prisma-cypress