DEV Community

Ayush
Ayush

Posted on

How To Set Up Next.js 15 For Production In 2025


I've worked on Next.js applications that scaled to over 100k monthly active users and landing pages with millions of monthly visitors. In this article, I'll share all the lessons I've learned through countless iterations.

Whether you're part of a small team of one, two, or three developers, or you work on a massive Next.js project with multiple teams on the same codebase, it's critical to get the foundation of your app right from the start. Even if you're working on an existing project, you will discover some hard-earned insights that you can apply to your app today.

Let me take you by the hand and show you all the steps and packages that you want to set up and explain to you why they're useful, so your app scales smoothly.

Initialize Your Project

Begin by creating a new Next.js project.

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

It may ask you if it can install the latest create-next-app version, just hit yes.

Need to install the following packages:
create-next-app@15.0.0
Ok to proceed? (y)
Enter fullscreen mode Exit fullscreen mode

And then configure your project by hitting yes on everything (TypeScript, Tailwind, app router).

✔ What is your project named? … reactsquad-production
✔ Would you like to use TypeScript? … No / Yes
Yes
✔ Would you like to use ESLint? … No / Yes
Yes
✔ Would you like to use Tailwind CSS? … No / Yes
Yes
✔ Would you like to use `src/` directory? … No / Yes
Yes
✔ Would you like to use App Router? (recommended) … No / Yes
Yes
✔ Would you like to customize the default import alias (@/*)? … No / No
Enter fullscreen mode Exit fullscreen mode

Then change into the directory of your project and open it in your favorite editor.

$ cd nextjs-for-production 
~/dev/nextjs-for-production (main) 🤯
$ cursor .
Enter fullscreen mode Exit fullscreen mode

Run The Development Server

You want to verify that your setup worked.

Type Checks With TypeScript

Your project has TypeScript already configured, but you also want to add an explicit command to your package.json that checks all your files for type errors.

package.json

"type-check": "tsc -b"
Enter fullscreen mode Exit fullscreen mode

You're going to use this command later together with other automated static analysis checks.

Code Formatting

When you collaborate with a large team on a project, it's important to unify the way that everyone writes code. Discuss choices like using semicolons, quote styles, and tabs versus spaces with your team.

Then use tools to enforce your style guide and format code automatically.

There are two tools for that: Prettier and ESLint.

Prettier

Prettier is an opinionated code formatter that eliminates style discussions during code reviews.

Install Prettier together with its Tailwind plugin.

npm install --save-dev prettier prettier-plugin-tailwindcss
Enter fullscreen mode Exit fullscreen mode

Create a prettier.config.js file with your preferred rules.

prettier.config.js

module.exports = {
  arrowParens: 'avoid',
  bracketSameLine: false,
  bracketSpacing: true,
  htmlWhitespaceSensitivity: 'css',
  insertPragma: false,
  jsxSingleQuote: false,
  plugins: ['prettier-plugin-tailwindcss'],
  printWidth: 80,
  proseWrap: 'always',
  quoteProps: 'as-needed',
  requirePragma: false,
  semi: true,
  singleQuote: true,
  tabWidth: 2,
  trailingComma: 'all',
  useTabs: false,
};
Enter fullscreen mode Exit fullscreen mode

Add a formatting script to your package.json.

package.json

"format": "prettier --write .",
Enter fullscreen mode Exit fullscreen mode

Run the formatter to apply your style.

$ npm run format

> nextjs-for-production@0.1.0 format
> prettier --write .

next.config.mjs 4ms (unchanged)
package-lock.json 54ms (unchanged)
package.json 1ms (unchanged)
postcss.config.mjs 2ms (unchanged)
README.md 20ms (unchanged)
src/app/globals.css 17ms (unchanged)
src/app/layout.tsx 30ms (unchanged)
src/app/page.tsx 11ms (unchanged)
tailwind.config.ts 2ms (unchanged)
tsconfig.json 2ms (unchanged)
Enter fullscreen mode Exit fullscreen mode

Your files are now "prettier", but you also want to use ESLint.

ESLint

ESLint can scan your code for both stylistic and logical issues. Install ESLint and its plugins, like unicorn, playwright and import sort.

npm install --save-dev @typescript-eslint/parser eslint-plugin-unicorn eslint-plugin-import eslint-plugin-playwright eslint-config-prettier eslint-plugin-prettier eslint-plugin-simple-import-sort
Enter fullscreen mode Exit fullscreen mode

Update your .eslintrc.json.

.eslintrc.json

{
  "extends": [
    "next/core-web-vitals",
    "plugin:unicorn/recommended",
    "plugin:import/recommended",
    "plugin:playwright/recommended",
    "plugin:prettier/recommended"
  ],
  "plugins": ["simple-import-sort"],
  "rules": {
    "simple-import-sort/exports": "error",
    "simple-import-sort/imports": "error",
    "unicorn/no-array-callback-reference": "off",
    "unicorn/no-array-for-each": "off",
    "unicorn/no-array-reduce": "off",
    "unicorn/prevent-abbreviations": [
      "error",
      {
        "allowList": {
          "e2e": true
        },
        "replacements": {
          "props": false,
          "ref": false,
          "params": false
        }
      }
    ]
  },
  "overrides": [
    {
      "files": ["*.js"],
      "rules": {
        "unicorn/prefer-module": "off"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This tutorial's plugins offer different functions. For detailed descriptions, visit their respective GitHub pages.

But in short, they enforce coding standards, organize imports, and ensure correct use of modern JavaScript features. Since ESLint and Prettier can conflict, this setup makes them work together smoothly. The plugins also help prevent bugs and keep styles consistent, especially with tools like Vitest and Playwright.

Add a linting script to your package.json.

package.json

"lint:fix": "next lint --fix",
Enter fullscreen mode Exit fullscreen mode

Run it to format all files according to your new rules.

$ npm run lint:fix

> nextjs-for-production@0.1.0 lint:fix
> next lint --fix

✔ No ESLint warnings or errors
Enter fullscreen mode Exit fullscreen mode

If you get a TypeScript version warning, you can ignore that.

Note: As of this article's writing, ESLint 9 is available, but this tutorial uses ESLint 8 because many plugins do not yet support the latest version.

Commitlint

When collaborating with a large team, it's also helpful to enforce consistent commit messages to keep the project history clear. By choosing the right standards, you can automate changelog and release generation with correct semantic versioning.

Install Commitlint and its necessary configurations. This includes Husky, which helps manage Git hooks.

npm install --save-dev @commitlint/cli@latest @commitlint/config-conventional@latest husky@latest
Enter fullscreen mode Exit fullscreen mode

Initialize Husky in your project to set up the basic configuration.

npx husky-init && npm install
Enter fullscreen mode Exit fullscreen mode

Add hooks to automate linting and type checking before each commit, and customize your commit message workflow.

npx husky add .husky/pre-commit 'npm run lint && npm run type-check'
npx husky add .husky/prepare-commit-msg 'exec < /dev/tty && npx cz --hook || true'
Enter fullscreen mode Exit fullscreen mode

The pre-commit hook runs after git commit, but before the commit message is finalized and runs linting and type-checking on your code.

The prepare-commit-msg hook runs after git commit is initiated but before the commit message editor opens. It runs commitizen CLI to let you craft conventional commit messages. You'll learn how to use this hook in a bit.

Remove the line that says npm test from .husky/_/pre-commit.

Make sure these scripts are executable.

chmod a+x .husky/pre-commit
chmod a+x .husky/prepare-commit-msg
Enter fullscreen mode Exit fullscreen mode

Here, chmod stands for "change mode." This command allows you to change the access permissions or modes of a file or directory in Unix and Unix-like operating systems. The argument a+x adds execute permissions for all users.

Install Commitizen, which provides a CLI for crafting conventional commit messages.

npm install --save-dev commitizen cz-conventional-changelog
Enter fullscreen mode Exit fullscreen mode

Configure Commitizen in your package.json to use the conventional changelog standard.

package.json

"config": {
  "commitizen": {
    "path": "cz-conventional-changelog"
  }
},
Enter fullscreen mode Exit fullscreen mode

The conventional changelog standard is a specification for adding human and machine-readable meaning to commit messages. It is designed to automate the producing of changelogs and releases based on the Git history of your project.

A future article will explain this standard in detail. It's included in this tutorial because getting it right from the start is important. You can use it and benefit from it without needing an in-depth understanding.

Create your commitlint.config.cjs file with rules that suit your team's needs. This setup ensures your commit messages are consistent and relevant to the changes made.

commitlint.config.cjs

const config = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'references-empty': [1, 'never'],
    'footer-max-line-length': [0, 'always'],
    'body-max-line-length': [0, 'always'],
  },
};

module.exports = config;
Enter fullscreen mode Exit fullscreen mode

Database With Prisma

For database management, use Prisma, which is an open-source ORM.

Install Prisma and initialize it.

npm install --save-dev prisma
npx prisma init
Enter fullscreen mode Exit fullscreen mode

Define your schema in 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
}
Enter fullscreen mode Exit fullscreen mode

Add the Prisma client and set up useful scripts for database management.

npm install @prisma/client
Enter fullscreen mode Exit fullscreen mode

package.json

"prisma:push": "prisma db push",
"prisma:studio": "prisma studio",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:wipe": "prisma migrate reset --force --skip-seed && prisma migrate deploy && prisma db seed",
Enter fullscreen mode Exit fullscreen mode

These scripts manage database schema migrations, data visualization through Prisma Studio, and database seeding.

Create a seed file to populate your database with initial data.

prisma/seed.ts

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

const prisma = new PrismaClient();

async function main() {
  await prisma.user.create({
    data: {
      email: 'jan@reactsquad.io',
      name: 'Jan Hesters',
    },
  });
}

main()
  .then(async () => {
    await prisma.$disconnect();
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });
Enter fullscreen mode Exit fullscreen mode

Update your package.json to point to the seed script.

package.json

"prisma": {
  "seed": "tsx prisma/seed.ts"
},
Enter fullscreen mode Exit fullscreen mode

Install tsx for running TypeScript files.

npm install --save-dev tsx
Enter fullscreen mode Exit fullscreen mode

Generate your Prisma client.

npx prisma generate
Enter fullscreen mode Exit fullscreen mode

Create a singleton instance of Prisma client to avoid connection issues.

src/lib/db.server.ts

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

const globalForPrisma = global as unknown as {
  prisma: PrismaClient | undefined;
};

export const db =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: ['query'],
  });

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
Enter fullscreen mode Exit fullscreen mode

Authentication With Clerk

For authentication, use Clerk. Sign up at clerk.com and create a new application.

Install the Clerk SDK.

npm install @clerk/nextjs
Enter fullscreen mode Exit fullscreen mode

Add your Clerk keys to .env.local.

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
Enter fullscreen mode Exit fullscreen mode

Wrap your app with ClerkProvider in your root layout.

src/app/layout.tsx

import { ClerkProvider } from '@clerk/nextjs';
import type { Metadata } from 'next';
import './globals.css';

export const metadata: Metadata = {
  title: 'Next.js for Production',
  description: 'Production-ready Next.js setup',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Create middleware to protect your routes.

src/middleware.ts

import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isProtectedRoute = createRouteMatcher(['/dashboard(.*)']);

export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) await auth.protect();
});

export const config = {
  matcher: [
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    '/(api|trpc)(.*)',
  ],
};
Enter fullscreen mode Exit fullscreen mode

Create a dashboard page to test authentication.

src/app/dashboard/page.tsx

import { db } from '@/lib/db.server';

export default async function DashboardPage() {
  const user = await db.user.findFirst();

  return (
    <div>
      <h1>Dashboard</h1>
      <p>{user?.name}</p>
      <p>{user?.email}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Testing With Vitest

Add Vitest for unit and integration testing.

Install Vitest and related dependencies.

npm install --save-dev vitest @vitejs/plugin-react vite-tsconfig-paths @vitest/coverage-v8
Enter fullscreen mode Exit fullscreen mode

Create a Vitest configuration file.

vitest.config.ts

import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    environment: 'jsdom',
    coverage: {
      provider: 'v8',
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Add test scripts to your package.json.

package.json

"test": "vitest",
"test:ui": "vitest --ui",
Enter fullscreen mode Exit fullscreen mode

Create a sample test file.

src/lib/db.server.test.ts

import { db } from '@/lib/db.server';
import { expect, test } from 'vitest';

test('db is defined', () => {
  expect(db).toBeDefined();
});
Enter fullscreen mode Exit fullscreen mode

Run your tests to verify the setup.

$ npm run test

> reactsquad-production@0.1.0 test
> vitest


 DEV  v2.1.8 /dev/reactsquad-production

 ✓ src/lib/db.server.test.ts (1)
   ✓ db is defined

 Test Files  1 passed (1)
      Tests  1 passed (1)
Enter fullscreen mode Exit fullscreen mode

Deployment With Vercel

Deploy your application to Vercel.

Install the Vercel CLI.

npm install -g vercel
Enter fullscreen mode Exit fullscreen mode

Link your project to Vercel.

vercel link
Enter fullscreen mode Exit fullscreen mode

Create a production database on Vercel.

  1. Go to your project on Vercel
  2. Navigate to Storage tab
  3. Click Create Database
  4. Select Postgres
  5. Choose a region (preferably close to your function region)
  6. Click Create

Then add POSTGRES_URL_NON_POOLING to the datasource in your Prisma schema.

prisma/schema.prisma

datasource db {
  provider  = "postgresql"
  // Uses connection pooling
  url       = env("DATABASE_URL")
  // Uses direct connection
  directUrl = env("POSTGRES_URL_NON_POOLING")
}
Enter fullscreen mode Exit fullscreen mode

Vercel uses connection poolers, which manage a pool of database connections that can be reused by different parts of an application. The directUrl property ensures operations requiring direct database access, such as migrations, can bypass the connection pooler for reliable execution.

You can get the environment variables for your Vercel database by pulling them from Vercel.

vercel env pull .env
Enter fullscreen mode Exit fullscreen mode

Playwright

Use E2E tests with Playwright because they give you the most confidence that your app works as intended.

Initialize Playwright.

$ npm init playwright@latest
Getting started with writing end-to-end tests with Playwright:
Initializing project in '.'
✔ Where to put your end-to-end tests? · playwright
✔ Add a GitHub Actions workflow? (y/N) · false
✔ Install Playwright browsers? (Y/n) · true
Enter fullscreen mode Exit fullscreen mode

Modify the webServer key in your playwright.config.ts file.

playwright.config.ts

webServer: {
  command: process.env.CI ? 'npm run build && npm run start' : 'npm run dev',
  port: 3000,
},
Enter fullscreen mode Exit fullscreen mode

Add test scripts to your package.json.

package.json

"test:e2e": "npx playwright test",
"test:e2e:ui": "npx playwright test --ui",
Enter fullscreen mode Exit fullscreen mode

The first one runs your Playwright test in headless mode, while the second one runs your test in UI mode with time travel debugging and watch mode.

Create a sample test.

playwright/example.spec.ts

import { expect, test } from '@playwright/test';

test.describe('dashboard page', () => {
  test('given any user: shows the test user', async ({ page }) => {
    await page.goto('/dashboard');

    await expect(page.getByText('Jan Hesters')).toBeVisible();
    await expect(page.getByText('jan@reactsquad.io')).toBeVisible();
  });
});
Enter fullscreen mode Exit fullscreen mode

Run your tests to verify the setup.

$ npm run test:e2e

> reactsquad-production@0.1.0 test:e2e
> npx playwright test

Running 3 tests using 3 workers
  3 passed (3.9s)
Enter fullscreen mode Exit fullscreen mode

GitHub Actions

Set up CI/CD with GitHub Actions.

Add a secret for your database URL to your repository's settings in GitHub.

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/testdb"
Enter fullscreen mode Exit fullscreen mode

Create your GitHub Actions workflow configuration.

.github/workflows/pull-request.yml

name: Pull Request

on: [pull_request]

jobs:
  lint:
    name: ⬣ ESLint
    runs-on: ubuntu-latest
    steps:
      - name: ⬇️ Checkout repo
        uses: actions/checkout@v3

      - name: ⎔ Setup node
        uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: 📥 Download deps
        uses: bahmutov/npm-install@v1

      - name: 🔬 Lint
        run: npm run lint

  type-check:
    name: ʦ TypeScript
    runs-on: ubuntu-latest
    steps:
      - name: ⬇️ Checkout repo
        uses: actions/checkout@v3

      - name: ⎔ Setup node
        uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: 📥 Download deps
        uses: bahmutov/npm-install@v1

      - name: 🔎 Type check
        run: npm run type-check --if-present

  commitlint:
    name: ⚙️ commitlint
    runs-on: ubuntu-latest
    if: github.actor != 'dependabot[bot]'
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - name: ⬇️ Checkout repo
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: ⚙️ commitlint
        uses: wagoid/commitlint-github-action@v4

  vitest:
    name: ⚡ Vitest
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:12
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
    steps:
      - name: ⬇️ Checkout repo
        uses: actions/checkout@v3

      - name: ⎔ Setup node
        uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: 📥 Download deps
        uses: bahmutov/npm-install@v1

      - name: 🛠 Setup Database
        run: npm run prisma:wipe
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb

      - name: ⚡ Run vitest
        run: npm run test -- --coverage
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb

  playwright:
    name: 🎭 Playwright
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:12
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
    steps:
      - name: ⬇️ Checkout repo
        uses: actions/checkout@v3

      - name: ⎔ Setup node
        uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: 📥 Download deps
        uses: bahmutov/npm-install@v1

      - name: 🌐 Install Playwright Browsers
        run: npx playwright install --with-deps

      - name: 🛠 Setup Database
        run: npm run prisma:wipe
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb

      - name: 🎭 Playwright Run
        run: npx playwright test
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb

      - name: 📸 Playwright Screenshots
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30
Enter fullscreen mode Exit fullscreen mode

Now every time you make a pull request to your app, it automatically runs your tests, type checks, and linting to ensure everyone contributes code with the same formatting and quality standards.


Conclusion

You now have a production-ready Next.js 15 setup with:

  • TypeScript for type safety
  • Prettier and ESLint for code quality
  • Commitlint and Husky for consistent Git workflow
  • Prisma for database management
  • Clerk for authentication
  • Vitest for unit testing
  • Playwright for E2E testing
  • GitHub Actions for CI/CD
  • Vercel for deployment

This foundation will help your application scale smoothly as your team and user base grow.


Want to learn more senior fullstack secrets? Subscribe to my newsletter for weekly updates on new videos, articles, and courses with exclusive bonus content and discounts.

Top comments (0)