Next.JS Starter Pack
Next.JS project setup and code quality toolings

Creating Next.JS project is as straight forward as using npx create-next-app app-name which is good for personal or small-scale projects but setting up a Next.JS project for large-team or professional setting can be tricky as there is a long list of configurations and package installations you need to setup. This blog was created to serve as a guide in setting up these kinds of Next.JS projects with the usage of ESLint, Prettier, Husky, Tailwind and Storybook.
1. Generate Project
-
Run the command on your terminal:
npx create-next-app <your-app-name> -
Open your project and delete both node_modules and package-lock.json followed by reinstalling your dependencies using yarn to create yarn.lock file. We will be using yarn instead of npm as package manager and deleting package-lock.json will avoid conflicts when installing other packages using yarn.
yarn install
2. Engine Locking
This part ensures that your colleagues adhere to the minimum node version and package manager to be used.
-
Create .nvmrc file in root directory and add the line below:
lts/gallium -
Create .npmrc file in root directory and add the line below:
engine-strict=true -
Add engines in package.json
... "engines": { "node": ">=16.0.0", "yarn": ">=1.22.0", "npm": "please-use-yarn" }, "scripts": { ...
3. ESLint
This section modifies ESLint configuration for linting code warnings, errors and suggestions.
-
Paste the lines below inside .eslintrc.json
{ "extends": ["next", "next/core-web-vitals", "eslint:recommended"], "globals": { "React": "readonly" }, "rules": { "no-unused-vars": [ 1, { "args": "after-used", "argsIgnorePattern": "^_" } ] } } -
Add the line below in package.json
"scripts": { ... "lint": "next lint" }
4. Prettier
Prettier formats code based on configured rules.
-
Install prettier as dev dependency
yarn add -D prettier -
Create prettier.config.js and paste the following:
module.exports = { trailingComma: 'es5', tabWidth: 2, semi: true, singleQuote: true, }; -
Create .prettierignore and paste the following:
.yarn .next .cache dist out node_modules next-env.d.ts next.config.ts package-lock.json public yarn.lock -
Add the line below in package.json
"scripts": { ... "prettier": "prettier --write ." }
5. Husky
Husky handles commands to be executed at certain git operations. Pre commit hook performs automatic code formatting and linting. Pre push hook executes build to ensures that the codebase is free of build errors before pushing to remote repository.
-
Execute the following commands
yarn add -D husky lint-staged npx husky install -
Add the line below in package.json
"scripts": { ... "prepare": "husky install" } -
Create pre-commit and pre-push hooks by executing commands below:
npx husky add .husky/pre-commit "yarn lint-staged" npx husky add .husky/pre-push "yarn build" -
Create lint-staged.config.js in root directory and paste the following:
module.exports = { // Check Typescript files '**/*.(ts|tsx)': () => 'yarn tsc --noEmit', // Lint and format TypeScript and JavaScript files '**/*.(ts|tsx|js)': (filenames) => [ `yarn eslint --fix ${filenames.join(' ')}`, `yarn prettier --write ${filenames.join(' ')}`, ], // Format MarkDown and JSON '**/*.(md|json)': (filenames) => `yarn prettier --write ${filenames.join(' ')}`, };
6. Conventional Commits Enforcement
Commit-msg hook ensures that the commit message follows conventional commits and blocks commit if there are any violations.
-
Install dev dependencies:
yarn add -D @commitlint/config-conventional @commitlint/cli -
Create commitlint.config.js in root directory and paste the following:
// build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) // ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) // docs: Documentation only changes // feat: A new feature // fix: A bug fix // perf: A code change that improves performance // refactor: A code change that neither fixes a bug nor adds a feature // style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) // test: Adding missing tests or correcting existing tests module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'body-leading-blank': [1, 'always'], 'body-max-line-length': [2, 'always', 100], 'footer-leading-blank': [1, 'always'], 'footer-max-line-length': [2, 'always', 100], 'header-max-length': [2, 'always', 100], 'scope-case': [2, 'always', 'lower-case'], 'subject-case': [ 2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case'], ], 'subject-empty': [2, 'never'], 'subject-full-stop': [2, 'never', '.'], 'type-case': [2, 'always', 'lower-case'], 'type-empty': [2, 'never'], 'type-enum': [ 2, 'always', [ 'build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test', 'translation', 'security', 'changeset', 'init', ], ], }, }; -
Execute one of the commands below
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"' # Sometimes above command doesn't work in some command interpreters # You can try other commands below to write npx --no -- commitlint --edit $1 # in the commit-msg file npx husky add .husky/commit-msg \"npx --no -- commitlint --edit '$1'\" # or npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"
7. VS Code Configuration
VS Code configuration handles workspace settings that will be used across team members who are also using VS Code.
-
Create .vscode folder in root directory
-
Create settings.json inside .vscode and paste the following for auto-formatting and organize imports on save:
{ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": true, "source.organizeImports": true } } -
Create launch.json inside .vscode and paste the following snippet for enabling NextJS vscode debugging:
{ "version": "0.1.0", "configurations": [ { "name": "Next.js: debug server-side", "type": "node-terminal", "request": "launch", "command": "npm run dev" }, { "name": "Next.js: debug client-side", "type": "pwa-chrome", "request": "launch", "url": "http://localhost:3000" }, { "name": "Next.js: debug full stack", "type": "node-terminal", "request": "launch", "command": "npm run dev", "console": "integratedTerminal", "serverReadyAction": { "pattern": "started server on .+, url: (https?://.+)", "uriFormat": "%s", "action": "debugWithChrome" } } ] } -
Create workspace.code-snippets inside .vscode and paste the following code for providing React and Next.JS code snippets:
{ "React TS Functional Component": { "prefix": ["rfc"], "body": [ "export interface ${1:$TM_FILENAME_BASE}Props {", " $2", "}\n", "const ${1:$TM_FILENAME_BASE}: React.FC<${1:$TM_FILENAME_BASE}Props> = (props) => {", " const { $3 } = props;\n", " return <div$0></div$0>;", "};\n", "export default ${1:$TM_FILENAME_BASE};\n" ], "description": "React TS functional component with props interface", "scope": "typescriptreact" }, "React TS Functional Component with default props": { "prefix": ["rfcd"], "body": [ "export interface ${1:$TM_FILENAME_BASE}Props {", " $2", "}\n", "const ${1:$TM_FILENAME_BASE}: React.FC<${1:$TM_FILENAME_BASE}Props> = (props) => {", " const { $4 } = props;\n", " return <div$0></div$0>;", "};\n", "${1:$TM_FILENAME_BASE}.defaultProps = {", " $3", "};\n", "export default ${1:$TM_FILENAME_BASE};\n" ], "description": "React TS functional component with props interface and default props", "scope": "typescriptreact" }, "React TS Mock Data": { "prefix": ["rfcm", "rfcmoc"], "body": [ "import { ${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}Props } from './${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}';\n", "const base: ${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}Props = {", " $0", "};\n", "export const mock${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}Props = {", " base,", "};\n" ], "description": "React TS functional component with props interface and default props", "scope": "typescriptreact,typescript" }, "React TS Storybook": { "prefix": ["rfcs"], "body": [ "import { ComponentMeta, ComponentStory } from '@storybook/react';", "import ${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}, { ${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}Props } from './${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}';", "import { mock${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}Props } from './${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}.mocks';\n", "export default {", " title: '${TM_DIRECTORY/^.+[\\/\\\\]+(.*)$/$1/}/${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}',", " component: ${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}},", " argTypes: {},", "} as ComponentMeta<typeof ${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}>;\n", "const Template: ComponentStory<typeof ${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}> = (args) => (", " <${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}} {...args} />", ");\n", "export const Base = Template.bind({});\n", "Base.args = {", " ...mock${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}Props.base,", "} as ${1:${TM_FILENAME_BASE/(.*)\\..+$/$1/}}Props;" ], "description": "React TS storybook snippet", "scope": "typescriptreact" }, "Next.js SSR FC": { "prefix": ["nxssr"], "body": [ "import { GetServerSideProps } from 'next';\n", "export interface ${1:$TM_FILENAME_BASE}Props {", " $2", "}\n", "const ${1:$TM_FILENAME_BASE}: React.FC<${1:$TM_FILENAME_BASE}Props> = (props) => {", " const { $4 } = props;\n", " return <div$0></div$0>;", "};\n", "export const getServerSideProps: GetServerSideProps<${1:$TM_FILENAME_BASE}Props> = async (", " context", ") => {", " return {", " props: { $3 },", " };", "};\n", "export default ${1:$TM_FILENAME_BASE};\n" ], "description": "Next.js SSR FC", "scope": "typescriptreact" }, "Next.js SSG FC": { "prefix": ["nxssg"], "body": [ "import { GetStaticProps } from 'next';\n", "export interface ${1:$TM_FILENAME_BASE}Props {", " $2", "}\n", "const ${1:$TM_FILENAME_BASE}: React.FC<${1:$TM_FILENAME_BASE}Props> = (props) => {", " const { $4 } = props;\n", " return <div$0></div$0>;", "};\n", "export const getStaticProps: GetStaticProps<${1:$TM_FILENAME_BASE}Props> = async (", " context", ") => {", " return {", " props: { $3 },", " };", "};\n", "export default ${1:$TM_FILENAME_BASE};\n" ], "description": "Next.js SSG FC", "scope": "typescriptreact" }, "Next.js SSG FC with Paths": { "prefix": ["nxssgp"], "body": [ "import { GetStaticPaths, GetStaticProps } from 'next';", "import { ParsedUrlQuery } from 'querystring';\n", "export interface ${1:$TM_FILENAME_BASE}Props {", " $4", "}\n", "export interface ${1:$TM_FILENAME_BASE}Path extends ParsedUrlQuery {", " $2", "}\n", "const ${1:$TM_FILENAME_BASE}: React.FC<${1:$TM_FILENAME_BASE}Props> = (props) => {", " const { $6 } = props;\n", " return <div$0></div$0>;", "};\n", "export const getStaticProps: GetStaticProps<${1:$TM_FILENAME_BASE}Props, ${1:$TM_FILENAME_BASE}Path> = async (", " context", ") => {", " return {", " props: { $5 },", " };", "};\n", "export const getStaticPaths: GetStaticPaths<${1:$TM_FILENAME_BASE}Path> = async () => {", " const paths = [{ params: { $3 } }];\n", " return {", " paths,", " fallback: false,", " };", "};\n", "export default ${1:$TM_FILENAME_BASE};\n" ], "description": "Next.js SSG FC with getStaticPaths", "scope": "typescriptreact" } }
8. Add Cross Env
Cross Env takes care of settings environment variables regardless of platform.
-
Install cross-env
yarn add -D cross-env -
Add modify dev script in package.json
"scripts": { "dev": "cross-env NODE_OPTIONS='--inspect' next dev", ... }
9. Tailwind
Tailwind is a utility-first CSS framework designed to speed up and make stylings a breeze.
-
Install and initialize tailwind and corresponding packages
yarn add -D tailwindcss postcss autoprefixer npx tailwindcss init -p -
Paste the following inside the generated tailwind.config.js:
/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './app/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}', // Or if using `src` directory: './src/**/*.{js,ts,jsx,tsx}', ], theme: { extend: {}, }, plugins: [], }; -
Add the following directives inside your globals.css file:
@tailwind base; @tailwind components; @tailwind utilities; -
Install prettier plugin for tailwind:
yarn add -D prettier-plugin-tailwindcss -
Update prettier.config.js to include the plugin:
module.exports = { ... plugins: [require('prettier-plugin-tailwindcss')], }
10. Storybook
Storybook provides isolated examination and testing of individual components without the need for integrating the component inside the DOM.
-
Make sure webpack is installed in your repository:
yarn add -D webpack -
Install storybook by executing command below:
npx storybook init -
Update .eslintrc.json. New file content is given below:
{ "extends": [ "plugin:storybook/recommended", "next", "next/core-web-vitals", "eslint:recommended" ], "globals": { "React": "readonly" }, "rules": { "no-unused-vars": [ 1, { "args": "after-used", "argsIgnorePattern": "^_" } ] } } -
Add resolutions in package.json. This is to fix problem with storybook 6.5 due to typescript 5
{ ... "resolutions": { "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.cd77847.0" } } -
Change build-storybook script with the following line below, again, due to the said issue in step 4:
"build-storybook": "cross-env NODE_OPTIONS=--openssl-legacy-provider build-storybook" -
Reexecute package installation to install resolution declaration:
yarn install -
Install two dependencies below to integrate tailwind to storybook:
yarn add -D @storybook/addon-styling postcss-loader -
Update main.js inside .storybook folder
module.exports = { stories: [ '../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)', ], addons: [ '@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions', { name: '@storybook/addon-styling', options: { postCss: true, }, }, ], framework: '@storybook/react', core: { builder: '@storybook/builder-webpack5', }, }; -
Update preview.js inside .storybook and make sure the breakpoints are aligned with the breakpoints you have for tailwind. The code below aligns with the default breakpoints of tailwind.
import * as NextImage from 'next/image'; import '../src/styles/globals.css'; const BREAKPOINTS_INT = { xs: 375, sm: 640, md: 768, lg: 1024, xl: 1280, '2xl': 1536, }; const customViewports = Object.fromEntries( Object.entries(BREAKPOINTS_INT).map(([key, val], idx) => { console.log(val); return [ key, { name: key, styles: { width: `${val}px`, height: `${(idx + 5) * 10}vh`, }, }, ]; }) ); // Allow Storybook to handle Next's <Image> component const OriginalNextImage = NextImage.default; Object.defineProperty(NextImage, 'default', { configurable: true, value: (props) => <OriginalNextImage {...props} unoptimized />, }); export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }, viewport: { viewports: customViewports }, }; -
execute yarn storybook to run and test storybook.
-
Delete generated stories folder.
Conclusion
With the toolings and configurations setup, better code quality and developer experience is assured. For summary/quick reference, you can check the gist by clicking this Link.
04 · 29.03.23The Making of Game of the GeneralsThe architecture and breakdown of Game of the Generals
03 · 29.03.23Next.JS Fullstack GraphQLImplementing fullstack GraphQL web app using Next.JS, GraphQL Yoga and Apollo Client
01 · 29.03.23Cryptography 101Cryptographic techniques and their implementations using NodeJS