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
- Install NextJS
- Configure TypeScript
- Customize PostCSS for Nesting and CSS Variables
- Add a CSS Reset
- Create a basic CSS theme using CSS Variables
- Configure Prettier
- Configure ESLint
- Configure Husky/Lint-Staged
- Absolute Import Paths
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!
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.
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't worry about what's in here;
// it's where the Next dev engine
// builds the contents it actually
// serves to you while you'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.
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:
- Autoprefixing
- Nesting
- 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?
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 fileeslint-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 writeany
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 asno-explicit-any
. If I’m confident that a value will always be defined in a given context even though, strictly speaking, it is potentiallynull
orundefined
, 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.js
1 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!
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.
Okay, okay, enough glowing. We’re not quiiiiiiiiiite done. We still have to set up our absolute import paths. But before we do that:
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.
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.