Cat logomenu

Save 20 Minutes Every Time with this Complete NextJS Config

A guide to starting all your new NextJS apps with PostCSS Theming, ESLint, Prettier, TypeScript, Babel module resolvers, and Lint-Staged/Husky already set up!

What We’re Gonna Do

The Whys and Wherefores

More and more people are choosing NextJS for their React apps for the same reasons I love it: Great SEO with Server-Side Rendering/static page generation, first-class TypeScript support, easy filesystem-based routing, and smart defaults that are easy to customize. But that doesn’t mean Next gives you everything you need right out of the box.

What’s missing?

Tooling, that’s what.

You know what I mean: Every new app means 20 minutes (at least!) looking up all the right packages and config formats and rules to set up in your project so you can get started writing the nice clean code that’s going to make your product ✨. If you’re like me, by the time you get to start actually, you know, making the app, half your enthusiasm has been burned off with the frustration of reading abstract documentation and fiddling with rules to get everything to work together.

No more, I say!

Bill Murray in Groundhog Day driving and saying 'I'm not going to live by their rules anymore'

By the time you’ve finished this guide, you’ll have a repo you can clone every time you start a new project. You can skip all that swearing and hair-pulling and get right down to coding. Let’s get started!

*Psssssst: If you don’t feel like following this tutorial and you want to just trust my choices, you can jump to the end and get the download link for my public repo.*

OK, for real… let’s go!

Install NextJS 

Open a terminal to the directory where you want to store your app. If you’re like me, you always forget, so here’s a reminder: If you want your app root to be my-bulletproof-app, you need to navigate in the terminal to whatever you want the parent folder of my-bulletproof-app to be.

Now run:

npx create-next-app

This will create the skeleton of the app for you, but you’re going to be asked to name your app. We’ll use "my-bulletproof-app", because why abandon a great name like this once we’ve thought of it?

After you’ve typed in your app name, sit back in your chair and take a sip of coff—oh, wait, it’s done already. (Seriously, it doesn’t take that long.) Change to the directory that create-next-app just created:

cd my-bulletproof-app

Now we’ll add TypeScript.

Configure TypeScript 

See? I told you we’d add TypeScript. Let’s do it. First, install the required packages:

yarn add —dev typescript @types/react @types/node

Once that’s done, you need to create a tsconfig.json file:

touch tsconfig.json

A cool thing about the NextJS dev engine is that it will automatically populate this file if we then run yarn dev. So, uh:

yarn dev

Now open your tsconfig.json and take a look at what you’ve got.

Next uses good defaults. We won’t change anything here. The only thing we don’t have at the moment is yummy module resolution, but we’ll add that later.

One last thing to get TypeScript set up. It’d be nice if Next did this for you, but you need change the extensions of the existing .js files in the pages directory to .tsx.

Go ahead and do it now; I’ll wait.

Alex Trebek dancing in front of the Final Jeopardy board, where the answer is 'The Supreme Court'

Okay, good. You’re back. Since you’re here, you’ll also want add typings to a couple of the /pages files:

1. _app.tsx. Delete everything that’s in there now and replace it with this:

import type { AppProps } from "next/app";
import "styles/globals.css";

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

export default MyApp;

2. api/hello.tsx. Honestly, you can delete this if you want, since it doesn’t really do anything. But I think it’s nice to have as a starter for your actual API later. Anyway, same business: replace the entire contents of this file with:

import { NextApiRequest, NextApiResponse } from "next";

export default (req: NextApiRequest, res: NextApiResponse) => {
  res.statusCode = 200;
  res.json({ name: "John Doe" });
};

At this point, the contents of your app’s root directory should look like this:

|— .next
	|— ???
	// Don&#39;t worry about what&#39;s in here;
    // it&#39;s where the Next dev engine
    // builds the contents it actually
    // serves to you while you&#39;re working.
|— node_modules
|— pages
	|— api
		|— hello.tsx
	|— _app.tsx
	|— index.tsx
|— public
	|—  favicon.ico
	|— vercel.svg
|— styles
	|— globals.css
	|— Home.module.css
|— .gitignore
|— next-env.d.ts
|— package.json
|— README.md
|— tsconfig.json
|— yarn.lock

At this point, you have a working NextJS + TypeScript app! Hurray for you! 🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉

Okay, what next?

CSSssssssssssssssss

Okay, I’m going to be opinionated at you for a minute.

The Dude from The Big Lebowski saying 'Yeah, well, that's just, like, your opinion, man.'

For a lot of projects, you don’t need UI libraries and CSS frameworks. UI libraries bloat your code, and if you need to adhere to a client’s custom designs, you’ll end up fighting them. CSS Frameworks can be nice, but a lot of times they’re just—to borrow a phrase—money laundering for the lack of easy scoping. And if you really want or need to write JS in your CSS, most of the time you can do it with a regular old style attribute.

If you disagree, feel free to skip this next part. Because we’re just going to use CSS Modules + PostCSS to style our app.

y tho

CSS Modules gets you scoping. Every className you use will get compiled into a unique CSS class, so you won’t have to worry about collisions. It’s awesome, and NextJS supports it out of the box.

PostCSS gives you all kinds of great stuff, depending on the plugins you install, but what we care about today are three things:

  1. Autoprefixing
  2. Nesting
  3. CSS Variables

Next also supports PostCSS out of the box, but only for autoprefixing. I don’t know about y’all, but I’m not interested in living in a world without CSS variables, especially since I’m not using a fancy framework that lets me write JS in my CSS.

Nesting is less important if you keep your components tidy, but I still want it. We’re going to add both of these features, so let’s do that first.

Customize PostCSS for Nesting and CSS Variables 

First, install the packages:

yarn add --dev postcss-custom-properties postcss-flexbugs-fixes postcss-nesting postcss-preset-env

(postcss-nesting and postcss-custom-properties are the additions we actually want. We need the other two packages because the moment you customize Next’s PostCSS config you lose all their built-in stuff and have to reimplement it yourself. Read more in their docs if you’re curious.)

Now we need to create a config file:

touch postcss.config.js

Open this file and paste in the following:

module.exports = {
  plugins: [
    [
      "postcss-custom-properties",
      {
        importFrom: "./styles/theme.css",
        preserve: false,
      },
    ],
    "postcss-flexbugs-fixes",
    "postcss-nesting",
    [
      "postcss-preset-env",
      {
        autoprefixer: {
          flexbox: "no-2009",
        },
        stage: 3,
        features: {
          "custom-properties": false,
        },
      },
    ],
  ],
};

This just re-implements Next’s default config and adds the rules for our nesting and CSS Variables (“custom properties”, because that’s what the official name for them used to be).

A couple things here:

  • We’re using importFrom: "./styles/theme.css" because in a just a moment we’re going to create a theme file to use across the application
  • We use preserve: false to prevent the variables from being included in our compiled CSS, because some browsers don’t take well to this

Okay, next we want to add some global styles and a theme. But wait! We can’t write predictable CSS without a reset. Let’s…

Add a CSS Reset 

If you’ve never used one before, the purpose of a CSS Reset is to even out the inconsistencies between different browser implementations of the CSS spec (read: “quirks”). If you don’t use a reset, you’ll occasionally find yourself grinding your teeth over some element that stubbornly refuses to be styled the way you expect, only to eventually realize that the browser is throwing some default padding or something onto it.

That’s silly. Let’s not have that.

I use a pretty basic, unopinionated reset, written by Eric Meyer way back in 2011, when dinosaurs roamed the internet. Open up your styles/globals.css file and delete everything in it. Then copy and paste this:

/* http://meyerweb.com/eric/tools/css/reset/
   v2.0 | 20110126
   License: none (public domain)
*/

/* prettier-ignore */
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup,  menu, nav, output, ruby, section, summary,
time, mark, audio, video {
	margin: 0;
	padding: 0;
	border: 0;
	font-size: 100%;
	font: inherit;
	vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
/* prettier-ignore */
article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {
	display: block;
}
body {
  line-height: 1;
}
ol,
ul {
  list-style: none;
}
blockquote,
q {
  quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
  content: "";
  content: none;
}
table {
  border-collapse: collapse;
  border-spacing: 0;
}

/**
  ****************
  * END CSS RESET
  ****************
**/

(If you’re wondering about the /\* prettier-ignore \*/ comments, that’s for later, to keep Prettier from splitting those long lists of selectors into multiple lines.)

Okay, now that we have a clean baseline of styles, we can move on to our theme!

Create a basic CSS theme using CSS Variables 

First, create the theme file:

touch styles/theme.css

Next, paste in my theme code, below. But really, this is where you should feel free to play around a lot—change my values, change the variable names, change the system for doing spacing… add lots of stuff. I kept it lean because each client project will have its own needs. You do what feels best to you.

Here’s the code:

:root {
  /* COLORS */
  --color-primary: #6fb2e0;
  --color-header: #333333;
  --color-link: #8888ee;
  --color-border: rgba(0, 0, 0, 0.25);
  --color-button-hover: salmon;
  --white: white;

  /* FONTS */
  --font-body: Helvetica, Arial, sans-serif;
  --font-header: Georgia, serif;

  /* SPACING */
  --spacing-base: 0.8rem;
  --spacing-1: calc(var(--spacing-base) * 1);
  --spacing-2: calc(var(--spacing-base) * 2);
  --spacing-3: calc(var(--spacing-base) * 3);
  --spacing-4: calc(var(--spacing-base) * 4);
  --spacing-5: calc(var(--spacing-base) * 5);
  --spacing-6: calc(var(--spacing-base) * 6);
  --spacing-7: calc(var(--spacing-base) * 7);
  --spacing-8: calc(var(--spacing-base) * 8);
  --spacing-9: calc(var(--spacing-base) * 9);
  --spacing-10: calc(var(--spacing-base) * 10);
  --spacing-11: calc(var(--spacing-base) * 11);
  --spacing-12: calc(var(--spacing-base) * 12);

  /* TYPOGRAPHY */
  --fs-base: 62.5%;
  --fs-normal: 1.6rem;
  --lh-normal: 1.2;
  --ls-normal: 0.02rem;
  --header-weight: bold;
  --header-lh: 1.5;
  --header-ls: 0.25rem;
  --header-align: center;
  --h1-size: 6rem;
  --h2-size: 4rem;
  --h3-size: 2rem;
  --h4-size: 1.8rem;
  --p-size: 1.6rem;
  --p-lh: 1.2;
  --p-ls: 0.2rem;
  --p-mb: var(--spacing-2);

  /* BORDERS */
  --radius-sm: 2px;
  --radius: 4px;
  --radius-md: 6px;
  --radius-lg: 10px;
  --border: 1px solid var(--color-border);

  /* SHADOWS */
  --box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14),
    0px 1px 3px 0px rgba(0, 0, 0, 0.12);
}

Now you can replace Next’s default styling in globals.css with styles that use our theme variables. Skip back to that file and paste this below the reset:

:root {
  font-size: var(--fs-base);
}

html,
body {
  font-family: var(--font-body);
}

a {
  color: var(--color-link);
  text-decoration: none;
}

* {
  box-sizing: border-box;
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: var(--font-header);
  font-weight: var(--header-weight);
  line-height: var(--header-lh);
  text-align: var(--header-align);
}

h1 {
  font-size: var(--h1-size);
  letter-spacing: var(--header-ls);
}

p,
span {
  font-size: var(--fs-normal);
  line-height: var(--lh-normal);
  letter-spacing: var(--ls-normal);
  margin-bottom: var(--spacing-2);
}

Again, feel free to customize at this point. I’m just leaving the default NextJS content in place, so I’ll leave their styles mostly intact as well.

You can now use your custom theme variables anywhere using the var(--variable-name) syntax you see above. Who needs CSS-in-JSS?

Bernie Sanders shaking his head and saying 'Nah. Not me.'

The one thing I’m not crazy about: Any time you change the values in the theme file, you have to shut down the NextJS dev engine and start it up again using yarn dev, because it’s only during build time that it reads in all the variables. If anyone figures out how to avoid this, holler at me.

Okay, at this point everything “works”. You could write your app, customize your theme with more CSS Variables, get all that delicious type-checking with TypeScript… basically, go out and change the world.

But you’ll still be missing one thing…

Cooooode Qualityyyyyyyyyyyy

You don’t want to write ugly-looking code. It’s hard to read later! You don’t want to await functions that don’t return Promises. You might write a race condition! You don’t want to accidentally leave the alt attributes out of your image. How will screen readers tell their users the content of the image?

You need to…

Keep your Code Clean and Beautiful

Configure Prettier 

Let’s start with formatting:

yarn add --dev prettier; touch .prettierrc

After these commands finish running, open your new .prettierrc file and paste in the following:

{
  "semi": false
}

If you like semicolons in your JS, you can skip even having a .prettierrc file. It seems to be more common these days to not use semicolons, so that’s what I’m doing here.

I would discourage messing around too much with the Prettier config, because that is literally why Prettier exists, but if you really want to, this is where you could dive into the Prettier default options and change the config.

Okay, easy enough. Now let’s…

Configure ESLint 

This one’s a doozy:

yarn add --dev eslint eslint-config-prettier eslint-plugin-import eslint-import-resolver-typescript eslint-plugin-import-helpers eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser

I won’t explain what every one of these does, but here are the highlights:

  • eslint-plugin-prettier/eslint-config-prettier tells ESLint to let Prettier handle the formatting.
  • eslint-plugin-import-helpers is going to keep your module imports in a consistent order. This makes it easier to visually scan the imports in a given file
    • eslint-plugin-react-hooks keeps you from violating the rules of hooks.
  • eslint-plugin-jsx-a11y warns you about common accessibility issues. Accessibility is important, so why not automate it?

Okay, now we configure our rules:

touch .eslintrc.js

Open this file and paste in this config:

module.exports = {
  env: {
    browser: true,
    node: true,
    es2020: true,
  },
  parser: "@typescript-eslint/parser",
  parserOptions: {
    tsconfigRootDir: __dirname,
    project: ["./tsconfig.json"],
    ecmaVersion: 2020,
    sourceType: "module",
    ecmaFeatures: {
      jsx: true,
    },
  },
  plugins: [
    "@typescript-eslint",
    "jsx-a11y",
    "react",
    "prettier",
    "eslint-plugin-import-helpers",
  ],
  extends: [
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking",
    "plugin:react/recommended",
    "plugin:import/errors",
    "plugin:import/warnings",
    "plugin:import/typescript",
    "plugin:jsx-a11y/recommended",
    "prettier",
    "prettier/@typescript-eslint",
    "prettier/react",
  ],
  rules: {
    "react/react-in-jsx-scope": "off",
    "jsx-a11y/anchor-is-valid": [
      "error",
      {
        components: ["Link"],
        specialLink: ["hrefLeft", "hrefRight"],
        aspects: ["invalidHref", "preferButton"],
      },
    ],
    "import-helpers/order-imports": [
      "warn",
      {
        newlinesBetween: "never",
        groups: [["parent", "sibling", "index"]],
        alphabetize: { order: "asc", ignoreCase: false },
      },
    ],
    "@typescript-eslint/explicit-module-boundary-types": "off",
    "@typescript-eslint/no-explicit-any": "off",
    "@typescript-eslint/no-non-null-assertion": "off",
    "react/jsx-filename-extension": [1, { extensions: [".tsx", "jsx"] }],
    "react/prop-types": 0,
  },
  settings: {
    react: {
      version: "detect",
    },
    "import/resolver": {
      "babel-module": {
        extensions: [".ts", ".tsx", ".js", ".jsx"],
      },
    },
  },
};

Again, I won’t do a deep dive here. For most of these plugins and rules or rulesets, just googling the name of the thing will get you an informative result. But I think the @typescript-eslint rules are worth covering because TypeScript is such an important part of writing code:

  • @typescript-eslint/explicit-module-boundary-types: This name is weird, but for my purposes it’s there to enforce explicit return types on functions. This keeps you from abusing a function by making it potentially return something different than you intended.
  • @typescript-eslint/no-explicit-any: Kinda what it sounds like. I think we should be able to write any types when we really need to. I trust myself to be careful about this, and you know what? I trust you, too. You and me, pal. You and me.
  • @typescript-eslint/no-non-null-assertion: Same philosophy as no-explicit-any. If I’m confident that a value will always be defined in a given context even though, strictly speaking, it is potentially null or undefined, I ought to be allowed to throw a ! after it to reassure the compiler.

Customize at your discretion.

At this point, we need to add an item to our tsconfig.json to make sure the compiler is aware of all these *.js config files we’ve created. Open that file and update the "include" line to look like this:

"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.*.js"],

Now we also need to give ESLint some instructions about what files to lint and not lint, so:

touch .eslintignore

Open that file and paste in:

node_modules
.next
!.eslintrc.js
!.babelrc.js

We don’t want to spend precious CPU cycles—and, more importantly, minutes of our time—linting imported or generated code like node_modules and .next. But we also do want to lint our .eslintrc.js and .babelrc.js1 files, which are typically ignored by default.

WHY do we want to lint them? Because I’m obsessive, okay? There, you made me say it!

Simpsons male character wearing apron on airliner flinching away from the camera and yelling 'Don't look at me'

Speaking of obsessive…

Leave Nothing to Chance! 

There are great plugins for the most popular code editors that will make it so the editor yells at you if you violate the linting rules. But you should also make sure that you don’t even allow yourself to commit code that’s misformatted or has errors. To do this, we’ll use husky and lint-staged:

yarn add --dev husky lint-staged

After this completes, open your package.json file and paste this in (I do it right after the "scripts"):

"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},
"lint-staged": {
  "./**/*.{js,jsx,ts,tsx,css,json}": [
    "prettier --config ./.prettierrc --write"
  ],
  "./**/*.{js,jsx,ts,tsx}": [
    "eslint --fix"
  ]
},

Some people like to add .lintstagedrc and .huskyrc files to keep this out of package.json, and if you want to do that you can consult the lint-staged and husky docs. Personally, I think it’s fine to keep it in here if your config is simple (which ours is!).

I also like to add some yarn scripts so I can lint/format/type-check manually if I want, so let’s update our "scripts" in package.json to look like this:

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "format": "prettier --write './**/*.{tsx,ts}'",
  "lint": "eslint --ext .ts,.tsx --fix --max-warnings 0 .",
  "type-check": "tsc -p ."
}

We’ll circle back to this later.

Okay, now we have automatic code-quality checks! Let’s all polish our developer badges and enjoy that glow of honest pride we’re all feeling.

Jiminy Cricket in Disney's Pinocchio smiling and showing off his sparkling badge

Okay, okay, enough glowing. We’re not quiiiiiiiiiite done. We still have to set up our absolute import paths. But before we do that:

Excuse me sir, do you have time to talk about our lord and savior, Lord Voldemort?

If this guide is helping you, please consider signing up for my mailing list! You’ll get an update straight to your inbox whenever I have a new post.

Okay, now we’re really ready to bring this home with…

Absolute Import Paths 

Maybe you’re familiar with this kind of thing:

import { MyComponent } from "../../../../components/MyComponent";

So. Gross.

We’re going to fix that with some nice, tidy, absolute paths, so you can just

import { MyComponent } from "components/MyComponent";

NOTE: NextJS has this built in now, but when I used it, VSCode would lose its mind every few minutes and fill my Problems pane with irrelevant errors. I ended up just rolling back to what we’re going to do:

yarn add --dev babel-plugin-module-resolver eslint-import-resolver-babel-module

Then:

touch .babelrc.js

Open your new .babelrc.js file2 and paste in:

module.exports = {
  presets: ["next/babel"],
  plugins: [
    [
      require.resolve("babel-plugin-module-resolver"),
      {
        root: ["."],
        alias: {
          components: "./components",
          pages: "./pages",
          public: "./public",
          styles: "./styles",
          util: "./util",
        },
      },
    ],
  ],
};

This is another place where you have to fully replicate Next’s config once you change it at all, so that’s what most of this is. The part that we really care about is that alias object. This is where we tell Babel what folders we’d like to have snappy absolute pathnames for, and what those paths should be called.

I’m pretty prosaic about this sort of thing, but if you wanted to call your components folder something like rainbowsandsunshine, you totally could. I guess you like to type a lot or something.

You might notice the util entry, and be thinking, “But Ryan” again. You’re right: We don’t have a util folder in our root directory. But I will. One day. It always ends up in there one way or another. So there it is. If you don’t plan to have such a thing, or you want to call it lib or helpers or daisiesandponies, do your magic.

Now, if you don’t tell the TypeScript compiler about all of this, it is going to flip out, so open your tsconfig.json and add the following to compilerOptions:

"baseUrl": ".",
"paths": {
  "components/*": ["./components/*"],
  "pages/*": ["./pages/*"],
  "public/*": ["./public/*"],
  "styles/*": ["./styles/*"],
  "util/*": ["./util/*"]
}

One last thing. ESLint will also yell at you if it doesn’t know where these aliases point to, so open your .eslintrc.js and update the settings value so it looks like this:

settings: {
  react: {
    version: "detect",
  },
  "import/resolver": {
    "babel-module": {
      extensions: [".ts", ".tsx", ".js", ".jsx"],
    },
  },
},

Finally (whew!), I like to make sure that the plugin that keeps my imports in order keeps all my aliases together in one group. Scroll up and find the groups value under the rules for "import-helpers/order-imports". Update groups to look like this:

groups: [
  "module",
  "/^components/",
  "/^pages/",
  "/^public/",
  "/^styles/",
  "/^util/",
  ["parent", "sibling", "index"],
],

Now you can import directly from any of these folders without all those pesky dots. Your long national nightmare is over.

Troy from Community standing up as he says 'Huzah!'

Wrap-Up

Now, before we really call it a day, I always think it’s good to run some basic checks, so run these commands one at a time:

yarn format
yarn lint
yarn type-check

If they all exit cleanly, you’re done! If not… well, it’s debug time. Hit me up on Twitter if you really run into trouble.

Now you can frolic on over to your cloud-hosted git provider of choice and create a new repo. Mine is nextjs-starter, in case you want to download it now.

Push our code on up to your new repo, and from now on, when you want to start a glorious new React application with SSR and static page generation, you don’t have to visit 34 different doc pages to remember how all this stuff is supposed to fit together. Just clone your own repo and get to typing!

Come at me

  • Do you have a bunch of thawts on my config? Bring me all your best bike-shedding @glassblowerscat!
  • I’ll be posting more NextJS-related goodness soon. Sign up for my newsletter if you want to get notified when a new post goes up.

Download the completed repo at: https://github.com/glassblowerscat/nextjs-starter. 


  1. “But Ryan”, I hear you saying, “we don’t have a .babelrc.js file.” Oh, just you wait, my friend.
  2. See?

Give us a share!