Rahino Quijano
Writing — 02

Next.JS Starter Pack

Next.JS project setup and code quality toolings

Published29.03.23Read10 minAuthorRahino Quijano
Next.JS Starter Pack

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

  1. Run the command on your terminal:

    npx create-next-app <your-app-name>
  2. 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.

  1. Create .nvmrc file in root directory and add the line below:

    lts/gallium
  2. Create .npmrc file in root directory and add the line below:

    engine-strict=true
  3. 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.

  1. 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": "^_" }
        ]
      }
    }
  2. Add the line below in package.json

    "scripts": {
     ...
     "lint": "next lint"
    }

4. Prettier

Prettier formats code based on configured rules.

  1. Install prettier as dev dependency

    yarn add -D prettier
  2. Create prettier.config.js and paste the following:

    module.exports = {
      trailingComma: 'es5',
      tabWidth: 2,
      semi: true,
      singleQuote: true,
    };
  3. 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
  4. 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.

  1. Execute the following commands

    yarn add -D husky lint-staged
    npx husky install
  2. Add the line below in package.json

    "scripts": {
     ...
     "prepare": "husky install"
    }
  3. 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"
  4. 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.

  1. Install dev dependencies:

    yarn add -D @commitlint/config-conventional @commitlint/cli
  2. 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',
          ],
        ],
      },
    };
  3. 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.

  1. Create .vscode folder in root directory

  2. 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
      }
    }
  3. 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"
          }
        }
      ]
    }
  4. 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.

  1. Install cross-env

    yarn add -D cross-env
  2. 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.

  1. Install and initialize tailwind and corresponding packages

    yarn add -D tailwindcss postcss autoprefixer
    npx tailwindcss init -p
  2. 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: [],
    };
  3. Add the following directives inside your globals.css file:

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
  4. Install prettier plugin for tailwind:

    yarn add -D prettier-plugin-tailwindcss
  5. 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.

  1. Make sure webpack is installed in your repository:

    yarn add -D webpack
  2. Install storybook by executing command below:

    npx storybook init
  3. 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": "^_" }
        ]
      }
    }
  4. 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"
      }
    }
  5. 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"
  6. Reexecute package installation to install resolution declaration:

    yarn install
  7. Install two dependencies below to integrate tailwind to storybook:

    yarn add -D @storybook/addon-styling postcss-loader
  8. 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',
      },
    };
  9. 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 },
    };
  10. execute yarn storybook to run and test storybook.

  11. 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.

Keep reading
Working on something like this? Let's talk