Skip to main content

Next.js CI/CD Pipeline

In modern web development, automating the testing and deployment of your applications is essential for maintaining quality and rapid delivery. In this tutorial, you'll learn how to set up a Continuous Integration and Continuous Deployment (CI/CD) pipeline for your Next.js applications.

What is CI/CD?

Continuous Integration (CI) is the practice of automatically integrating code changes from multiple contributors into a shared repository, where automated builds and tests verify each integration.

Continuous Deployment (CD) automatically deploys all code changes to a testing and/or production environment after the build stage.

Together, CI/CD creates an automated pipeline that improves developer productivity and helps catch bugs early.

Why implement CI/CD for Next.js applications?

  • Consistency: Every code change goes through the same testing process
  • Early bug detection: Automated tests catch issues before they reach production
  • Faster releases: Automate tedious manual deployment steps
  • Better collaboration: Team members can integrate their changes frequently

Setting up a CI/CD Pipeline for Next.js

We'll cover how to set up a CI/CD pipeline using GitHub Actions, which is free for public repositories and includes limited free minutes for private repositories.

Prerequisites

  • A Next.js application
  • A GitHub repository for your project
  • Basic understanding of Git
  • A deployment platform (we'll use Vercel in this tutorial)

Creating a Basic GitHub Actions Workflow

Let's start by creating a basic CI workflow that runs tests whenever code is pushed to the repository.

  1. Create a directory structure in your project:
bash
mkdir -p .github/workflows
  1. Create a new file .github/workflows/ci.yml:
yaml
name: Next.js CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Build
run: npm run build

- name: Test
run: npm test

This workflow does the following:

  • Triggers on pushes to the main branch and pull requests targeting main
  • Uses an Ubuntu environment
  • Sets up Node.js 18
  • Installs dependencies with npm ci (faster and more reliable than npm install)
  • Runs linting, building, and testing scripts

Understanding the workflow file

  • name: A name for your workflow
  • on: Defines when the workflow runs
  • jobs: Groups the jobs to run
  • build: The job ID
  • runs-on: The type of runner to use
  • steps: The sequence of tasks to execute

Adding Automated Testing

Testing is a crucial component of CI. Let's set up Jest for testing our Next.js components.

  1. Install Jest and testing libraries:
bash
npm install --save-dev jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
  1. Create a jest.config.js file:
javascript
const nextJest = require('next/jest')

const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files
dir: './',
})

// Add any custom config to be passed to Jest
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/pages/(.*)$': '<rootDir>/pages/$1',
},
testEnvironment: 'jest-environment-jsdom',
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)
  1. Create a jest.setup.js file:
javascript
import '@testing-library/jest-dom/extend-expect'
  1. Add test scripts to your package.json:
json
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}
  1. Create a simple test file __tests__/Home.test.js:
javascript
import { render, screen } from '@testing-library/react'
import Home from '../pages/index'

describe('Home page', () => {
it('renders without crashing', () => {
render(<Home />)
expect(screen.getByRole('heading')).toBeInTheDocument()
})
})

Setting up Continuous Deployment with Vercel

Vercel is a deployment platform that works excellently with Next.js (since Next.js is developed by Vercel). Let's set up CD with Vercel:

  1. Sign up for a Vercel account at vercel.com
  2. Install the Vercel CLI:
bash
npm install -g vercel
  1. Log in to Vercel from the CLI:
bash
vercel login
  1. Create a .github/workflows/cd.yml file:
yaml
name: Next.js CD

on:
push:
branches: [ main ]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
  1. Generate your Vercel token and project details:
bash
vercel whoami
vercel projects
  1. Add the secrets to your GitHub repository:
    • Go to your GitHub repository
    • Navigate to Settings > Secrets and variables > Actions
    • Add the following secrets:
      • VERCEL_TOKEN: Your Vercel token
      • ORG_ID: Your Vercel organization ID
      • PROJECT_ID: Your Vercel project ID

Creating a Complete CI/CD Pipeline

Now, let's integrate both CI and CD into a single workflow file:

yaml
name: Next.js CI/CD

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Build
run: npm run build

- name: Test
run: npm test

deploy:
needs: build-and-test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'

Key points in this workflow:

  • The deploy job depends on successful completion of build-and-test (needs: build-and-test)
  • Deployment only happens for pushes to the main branch, not pull requests (if: github.ref == 'refs/heads/main' && github.event_name == 'push')

Adding Environment-Specific Deployments

For a more advanced setup, you might want different environments:

yaml
name: Next.js CI/CD Pipeline

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
# Same as before

deploy-preview:
needs: build-and-test
if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to Preview
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}

deploy-production:
needs: build-and-test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to Production
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'

Real-World Example: Integrating Cache and Performance Checks

Here's a more comprehensive workflow with cache optimization and web performance checks:

yaml
name: Production CI/CD

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build-test-deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'npm'

- name: Get npm cache directory
id: npm-cache-dir
run: echo "::set-output name=dir::$(npm config get cache)"

- name: Cache node modules
uses: actions/cache@v3
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Build
run: npm run build

- name: Test
run: npm test

- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
uploadArtifacts: true
temporaryPublicStorage: true
runs: 3
configPath: './.github/workflows/lighthouserc.json'

- name: Deploy to Vercel
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'

And create a .github/workflows/lighthouserc.json file:

json
{
"ci": {
"collect": {
"staticDistDir": "./out",
"url": ["http://localhost:3000"]
},
"assert": {
"assertions": {
"categories:performance": ["error", {"minScore": 0.8}],
"categories:accessibility": ["warn", {"minScore": 0.9}]
}
}
}
}

Summary

You've now learned how to create a CI/CD pipeline for your Next.js applications using GitHub Actions and Vercel. This setup will:

  1. Automatically run tests when code is pushed or pull requests are created
  2. Deploy to preview environments for development branches
  3. Deploy to production for the main branch
  4. Include performance and accessibility checks

By implementing a CI/CD pipeline, you'll save time, catch issues early, and maintain a high-quality codebase.

Additional Resources

Exercises

  1. Set up a basic CI workflow for your Next.js project that runs linting and tests
  2. Add a step to your workflow that checks for bundle size increases
  3. Configure preview deployments for feature branches
  4. Implement end-to-end tests using Cypress and add them to your CI pipeline
  5. Set up notifications (Slack, email, etc.) for failed deployments

These exercises will help you become more familiar with CI/CD pipelines and ensure your Next.js applications are always production-ready.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)