Getting Started with Next.js 13.4
How I set up a project
5 August 2023
Written by Gareth
6 min read
This is a step-by-step guide to make a Next.js project with:
- TypeScript for type safety
- Tailwind CSS for styling
- Jest for testing components etc.
- Cypress for end to end testing etc.
- ESLint for linting
- Prettier for formatting
- Husky & Lint-Staged for pre-commit testing and linting
If you like this, checkout my other projects on GitHub or via my Portfolio
You can also read this guide on dev.to
TLDR: This setup is available as a template in my GitHub Account if you want the quickest path to the end result or want to see the setup in context- GLD-NextTemplate
Installation and Repo setup
npx create-next-app@latest
code my-app
git init
gh repo create
git add .
git commit . -m "Initial Commit"
git push -u origin main
Setup Testing
Jest
- Install Jest, ts-jest, jsdom, testing libraries & eslint plugins:
npm i -D jest ts-jest jest-environment-jsdom @testing-library/jest-dom @testing-library/react eslint-plugin-jest-dom eslint-plugin-testing-library
- Add jest.config.mjs
import nextJest from "next/jest.js";
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: "./",
});
// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config = {
// Add more setup options before each test is run
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
testEnvironment: "jest-environment-jsdom",
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config);
- Add jest.setup.js:
import '@testing-library/jest-dom/extend-expect'
import React from 'react'
function MockImage(props) {
return React.createElement('img', props)
}
jest.mock('next/image', () => MockImage)
- Add scripts to package.json:
"scripts": {
"test": "clear && jest",
"test:noClear": "jest",
"test:watch": "clear && jest --watchAll",
}
- Add first test in
\_\_tests\_\_/home.test.tsx
:
import React from 'react';
import { render } from '@testing-library/react';
import Home from '@/app/page';
describe('Home component', () => {
it('renders correctly', () => {
render(<Home />);
const heroHeading = screen.getByTestId('hero-heading');
expect(heroHeading).toBeInTheDocument();
const madeByText = screen.getByText(/Made by GLD5000/i);
expect(madeByText).toBeInTheDocument();
});
});
Cypress
-
Install latest Cypress:
npm i -D cypress@latest
(currently "cypress": "^12.17.3" at time of writing) -
Start a dev server in a terminal:
npm run dev
-
Open Cypress from a second terminal:
npx cypress open
-
Once open, click 'E2E Testing' button to configure and then click 'continue' (if you get an error relating to 'bundle' you may need to switch tsconfig.json
"moduleResolution": "bundler",
to"moduleResolution": "node",
), when prompted for a browser choose e.g. Chrome and click to add scaffolded examples if you like. -
Setup new config files within the 'cypress' folder:
cypress/eslintrc.json with disabled rules to accommodate cypress example specs:
{
"plugins": ["cypress"],
"extends": ["plugin:cypress/recommended"],
"rules": {
"testing-library/await-async-utils": "off",
"no-unused-expressions": "off",
"@typescript-eslint/ban-ts-comment": "off",
"cypress/unsafe-to-chain-command": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-empty-function": "off",
"func-names": "off",
"@typescript-eslint/no-var-requires": "off",
"global-require": "off",
"testing-library/no-debugging-utils": "off",
"no-param-reassign": "off"
},
"overrides": [
{
"files": ["viewport.cy.js", "waiting.cy.js"],
"rules": {
"cypress/no-unnecessary-waiting": "off"
}
}
]
}
cypress/tsconfig.json:
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
- Update exclude and include in root tsconfig.json:
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts","cypress/tsconfig.json"],
"exclude": ["node_modules", "./cypress.config.ts","cypress"]
- Put an eslint disable comment in the cypress.config.ts:
/* eslint-disable */
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});
- Add script to package.json:
"scripts": {
...
"cypress": "npx cypress open"
}
- Add first test:
- Add
data-testid
attribute to H1 on app/page.tsx:
<h1 data-testid="hero-heading" className="w-fit mx-auto text-5xl font-bold text-center">
Minimal Starter Template for
</h1>
- Add test in cypress/e2e/home.cy.ts:
describe("template spec", () => {
it("H1 contains correct text", () => {
cy.visit("http://localhost:3000/");
cy.get("[data-testid='hero-heading']").contains("Minimal Starter Template for");
});
});
Setup Linting
- Install prettier: npm i -D prettier eslint-plugin-prettier eslint-config-prettier
- Make prettier config e.g.:
touch .prettierrc.js
- Configure prettier e.g.:
export default {
trailingComma: "es5",
tabWidth: 4,
semi: false,
singleQuote: true,
};
- Setup eslint:
npm init @eslint/config
- Install Eslint AirBnb:
npm i -D eslint-config-airbnb
- Get peer dependency list for AirBnb config: list peer dependencies
npm info "eslint-config-airbnb@latest"
e.g.:
eslint: '^7.32.0 || ^8.2.0',
'eslint-plugin-import': '^2.25.3',
'eslint-plugin-jsx-a11y': '^6.5.1',
'eslint-plugin-react': '^7.28.0',
'eslint-plugin-react-hooks': '^4.3.0'
- Paste into package.json devDependencies and watch for conflicts (duplicates) you will not need
eslint
if it is already installed with Next.js. - Run npm install to install the extra dependencies:
npm i
- Install Eslint typescript:
npm i -D @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-airbnb-typescript
- Install testing plugins:
npm i -D eslint-plugin-jest-dom eslint-plugin-testing-library
- Update eslint config e.g.:
module.exports = {
extends: [
'next/core-web-vitals',
'plugin:testing-library/react',
'plugin:jest-dom/recommended',
'airbnb',
'prettier',
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:@typescript-eslint/recommended',
],
plugins: ['prettier', 'react'],
ignorePatterns: ['*.ttf', '*.css'],
rules: {
'@next/next/no-img-element': 'off',
'import/no-extraneous-dependencies': 'off',
'react/require-default-props': 'off',
'react/jsx-props-no-spreading': 'off',
'react/jsx-no-useless-fragment': 'off',
'no-undef': 'off',
'import/extensions': 'off',
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
'no-console': 'off',
'react/jsx-no-bind': 'off',
'react/prop-types': 'off',
'no-use-before-define': ['error', { functions: false }],
'react/jsx-filename-extension': 'off',
'prettier/prettier': ['error'],
'jsx-a11y/label-has-associated-control': [
2,
{
controlComponents: [
'InputSelect',
'SectionTitle',
'InputTitle',
'InputText',
],
depth: 3,
},
],
},
}
Setup Linting Pre Hooks
npm i -D husky
npm pkg set scripts.prepare="husky install"
npm i
ornpm run prepare
npm install -D lint-staged
- Add hook:
npx husky add .husky/pre-commit "npx lint-staged"
- Make lint-staged configuration file:
touch lint-staged.config.js
- Add lint-staged configuration e.g.:
module.exports = {
// this will check Typescript files
'**/*.(ts|tsx)': () => 'yarn tsc --noEmit',
// This will format and lint TS & JS Files
'**/*.(ts|tsx|js)': (filenames) => [
`yarn prettier --write ${filenames.join(' ')}`,
`yarn eslint --fix --max-warnings=0 ${filenames.join(' ')}`,
],
// this will Format MarkDown and JSON
'**/*.(md|json)': (filenames) =>
`yarn prettier --write ${filenames.join(' ')}`,
}
- Add optional testing hook e.g.:
npx husky add .husky/pre-commit "npm run test:noClear"
so your pre-commit file could look something like this:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run test:noClear
npx lint-staged
Setup Additional Scripts
Package.json:
"lint": "clear && next lint",
"test": "clear && jest",
"test:noClear": "jest",
"test:watch": "clear && jest --watchAll",
"test:v": "clear && jest --verbose",
"format": "clear && prettier \"src/**/*.{js,jsx,ts,tsx,css,scss}\" --write",
"lint:fix": "clear && prettier \"src/**/*.{js,jsx,ts,tsx,css,scss}\" --write && eslint src --ext .js,.jsx,.ts,.tsx --fix",
"lint:all": "clear && prettier \"src/**/*.{js,jsx,ts,tsx,css,scss}\" --write && eslint src --ext .js,.jsx,.ts,.tsx --fix && tsc --noEmit"
That's It!
Not exactly a short process but this will give a really useful base to build quality code with. Don't forget to like / star if you found this useful! If you like, you can also check out my other projects and blogs.