My Next.js App Setup Checklist (2025 Edition)

11 min read
nextjsemotion

Table of Contents

Intro

Every time I start a new Next.js project, I find myself going through the same setup routine. Instead of constantly googling "how to configure Prettier with Next.js" or "Emotion cache provider setup" for the hundredth time, I decided to document my go-to configuration once and for all.

This is my personal checklist that gets me from create-next-app to a fully configured development environment. Maybe it'll save you some time too.

Quick Package Installation

Before diving into the configs, here are all the packages you'll need:

1# Runtime dependencies
2npm install @emotion/css @emotion/react @emotion/server @emotion/styled
3
4# Development dependencies
5npm install -D eslint eslint-config-next eslint-config-prettier eslint-plugin-unused-imports @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier stylelint stylelint-config-recess-order stylelint-config-standard stylelint-order stylelint-no-unsupported-browser-features postcss-styled-syntax

1. Set up Prettier

First things first - consistent code formatting. I always start with Prettier because it eliminates so many formatting debates.

Install packages:

1npm install -D prettier eslint-config-prettier

Create prettier.config.js:

1/** @type {import('prettier').Config} */
2module.exports = {
3 tabWidth: 2,
4 semi: false,
5 singleQuote: true,
6 trailingComma: 'all',
7}

This config gives you clean, consistent formatting with no semicolons and trailing commas everywhere (which makes for cleaner git diffs).

2. Configure ESLint

ESLint is where the magic happens - it catches bugs before they make it to production and enforces consistency across your codebase. Here's my battle-tested configuration that focuses on meaningful rules with clear purposes.

Install packages:

1npm install -D eslint eslint-config-next eslint-config-prettier eslint-plugin-unused-imports @typescript-eslint/eslint-plugin @typescript-eslint/parser

Create eslint.config.js:

1import path from 'node:path'
2import { fileURLToPath } from 'node:url'
3import typescriptEslint from '@typescript-eslint/eslint-plugin'
4import typescriptEslintParser from '@typescript-eslint/parser'
5
6import { FlatCompat } from '@eslint/eslintrc'
7import js from '@eslint/js'
8import unusedImports from 'eslint-plugin-unused-imports'
9
10const __filename = fileURLToPath(import.meta.url)
11const __dirname = path.dirname(__filename)
12const compat = new FlatCompat({
13 baseDirectory: __dirname,
14 recommendedConfig: js.configs.recommended,
15 allConfig: js.configs.all,
16})
17
18// Shared rules for all files
19const sharedRules = {
20 // Performance & Bundle Size
21 'unused-imports/no-unused-imports': 'error', // Unused imports increase bundle size
22 'no-unused-vars': 'off', // Turn off base rule to avoid conflicts
23 'unused-imports/no-unused-vars': [
24 'warn',
25 {
26 vars: 'all',
27 varsIgnorePattern: '^_',
28 args: 'after-used',
29 argsIgnorePattern: '^_',
30 },
31 ],
32
33 // Code Quality & Bug Prevention
34 'no-console': ['error', { allow: ['warn', 'dir'] }], // Prevent console.log in production, allow debugging tools
35 'no-restricted-syntax': ['error', 'TSEnumDeclaration', 'WithStatement'], // Prevent problematic syntax patterns
36 'no-unused-expressions': 'error', // Catch typos like `foo.bar` instead of `foo.bar()`
37 'no-debugger': 'error', // Prevent debugger statements in production
38 'no-alert': 'error', // Prevent alert() usage in production
39 'no-var': 'error', // Enforce let/const over var
40 'prefer-const': 'error', // Use const when variables aren't reassigned
41
42 // TypeScript rules (turned off for compatibility)
43 '@typescript-eslint/interface-name-prefix': 'off', // Allow flexible interface naming
44 '@typescript-eslint/member-delimiter-style': 'off', // Don't enforce semicolons in interfaces
45 '@typescript-eslint/no-empty-interface': 'off', // Sometimes empty interfaces are useful as placeholders
46
47 // React Best Practices
48 'react/self-closing-comp': ['error', { component: true, html: true }], // <Component /> instead of <Component></Component>
49
50 // Import Organization - Clean imports = easier navigation
51 'import/order': [
52 'error',
53 {
54 groups: [
55 'builtin', // node:fs, node:path
56 'external', // react, next, lodash
57 'type', // import type { ... }
58 'internal', // @/components, ~/utils
59 'sibling', // ./component
60 'parent', // ../component
61 'index', // ./
62 'object', // import log = console.log
63 ],
64 'newlines-between': 'always',
65 pathGroupsExcludedImportTypes: ['react'],
66 alphabetize: { order: 'asc', caseInsensitive: true },
67 pathGroups: [{ pattern: 'react', group: 'external', position: 'before' }], // React always goes first
68 },
69 ],
70}
71
72const config = [
73 ...compat.extends('next/core-web-vitals'),
74 {
75 files: ['**/*.{js,jsx,ts,tsx}'],
76 languageOptions: {
77 parser: typescriptEslintParser,
78 parserOptions: {
79 ecmaVersion: 'latest',
80 sourceType: 'module',
81 ecmaFeatures: { jsx: true },
82 },
83 },
84 plugins: {
85 'unused-imports': unusedImports,
86 '@typescript-eslint': typescriptEslint,
87 },
88 rules: sharedRules,
89 },
90]
91
92export default config

Why These Rules Matter

Bundle Size & Performance:

  • unused-imports/no-unused-imports - Unused imports get bundled even if never used, bloating your JavaScript bundle. This catches imports you forgot to remove after refactoring.
  • no-unused-vars - Dead code elimination works better when there are no unused variables lingering around.

Bug Prevention:

  • no-console - Console logs in production can leak sensitive info and slow down performance. We allow warn and dir for debugging.
  • no-restricted-syntax - TypeScript enums can cause unexpected behavior and bundle bloat; with statements are just evil.
  • prefer-const - Catches bugs where you accidentally reassign variables that should be constants.

Developer Experience:

  • import/order - Consistent import ordering makes it easier to find what you're looking for and reduces merge conflicts. React always goes first, then external packages, then your internal code, all alphabetized.
  • react/self-closing-comp - <Button active /> is cleaner than <Button active></Button>

The config is intentionally minimal - it focuses on catching real bugs and maintaining consistency without being overly restrictive. Sometimes the best ESLint config is the one that stays out of your way while preventing actual problems.

3. Install and Configure Emotion

For styling, I use Emotion. It gives me the flexibility of CSS-in-JS with great performance and TypeScript support.

Install packages:

1npm install @emotion/css @emotion/react @emotion/server @emotion/styled

The EmotionRegistry is technically optional, but I highly recommend it. Without it, you'll get that dreaded "flash of unstyled content" (FOUC) where your page looks broken for a split second on initial load. The registry ensures your CSS is properly cached and rendered server-side, so users see a fully styled page immediately.

Create lib/EmotionRegistry.tsx:

1'use client'
2
3import { useState } from 'react'
4import type { ReactNode } from 'react'
5
6import createCache from '@emotion/cache'
7import { CacheProvider } from '@emotion/react'
8import { useServerInsertedHTML } from 'next/navigation'
9
10export default function EmotionRegistry({ children }: { children: ReactNode }) {
11 const [cache] = useState(() => {
12 const cache = createCache({
13 key: 'css',
14 prepend: true,
15 })
16 cache.compat = true
17 return cache
18 })
19
20 useServerInsertedHTML(() => {
21 return (
22 <style
23 data-emotion={`${cache.key} ${Object.keys(cache.inserted).join(' ')}`}
24 dangerouslySetInnerHTML={{
25 __html: Object.values(cache.inserted).join(' '),
26 }}
27 />
28 )
29 })
30
31 return <CacheProvider value={cache}>{children}</CacheProvider>
32}

Add to layout.tsx:

1import EmotionRegistry from './lib/EmotionRegistry'
2
3export default function RootLayout({
4 children,
5}: {
6 children: React.ReactNode
7}) {
8 return (
9 <html lang="en">
10 <body>
11 <EmotionRegistry>{children}</EmotionRegistry>
12 </body>
13 </html>
14 )
15}

4. Configure Stylelint for CSS-in-JS

Even with CSS-in-JS, you still want to catch CSS mistakes and maintain consistency. Stylelint works great with Emotion through the postcss-styled-syntax plugin, and these rules are specifically tuned for performance and CSS-in-JS compatibility.

Install packages:

1npm install -D stylelint stylelint-config-standard stylelint-config-recess-order stylelint-order stylelint-no-unsupported-browser-features postcss-styled-syntax

Create stylelint.config.js:

1/** @type {import('stylelint').Config} */
2module.exports = {
3 extends: ['stylelint-config-standard', 'stylelint-config-recess-order'],
4 plugins: ['stylelint-order', 'stylelint-no-unsupported-browser-features'],
5 customSyntax: 'postcss-styled-syntax', // This makes Stylelint understand CSS-in-JS
6 ignoreFiles: ['**/node_modules/**', 'src/styles/resetCss/resetCss.ts'],
7 rules: {
8 // CSS-in-JS compatibility - these rules don't play well with CSS-in-JS
9 'selector-class-pattern': null, // CSS-in-JS generates class names dynamically
10 'alpha-value-notation': null, // Modern alpha notation can break in some contexts
11 'color-function-notation': 'legacy', // Use rgb() instead of rgb(255 0 0) for better compatibility
12 'no-descending-specificity': null, // CSS-in-JS handles specificity differently
13 'property-no-vendor-prefix': null, // Sometimes you need vendor prefixes for cutting-edge features
14 'value-no-vendor-prefix': null,
15 'nesting-selector-no-missing-scoping-root': null, // CSS-in-JS handles scoping automatically
16
17 /* --- Performance & Bundle Size --- */
18 // Block render-blocking @import in CSS - use JavaScript imports instead
19 'at-rule-disallowed-list': ['import'],
20
21 // Keep CSS small & selector matching fast
22 'selector-max-compound-selectors': 4, // .a .b .c .d is OK; deeper selectors hurt performance
23 'selector-max-combinators': 3, // Limit descendant/child/sibling chains
24 'selector-max-specificity': '0,4,0', // Avoid overly specific selectors that are hard to override
25 'selector-max-class': 4, // Too many classes in one selector gets unwieldy
26 'selector-max-id': 0, // IDs have too much specificity and aren't reusable
27 'selector-max-universal': 0, // Universal selectors (*) are performance killers
28 'selector-no-qualifying-type': [true, { ignore: ['attribute', 'class'] }], // div.class is redundant
29
30 // Avoid CSS mistakes that bloat bundles
31 'declaration-block-no-duplicate-properties': [
32 true,
33 { ignore: ['consecutive-duplicates-with-different-values'] }, // Allow fallbacks like display: flex; display: grid;
34 ],
35 'declaration-block-no-shorthand-property-overrides': true, // margin-top shouldn't override margin
36 'declaration-no-important': true, // !important makes CSS harder to maintain and debug
37
38 // Tame nesting (important for CSS-in-JS readability)
39 'max-nesting-depth': 3, // Deep nesting creates specificity wars and hard-to-debug CSS
40
41 // Warn about features that need heavy polyfills (keeps bundles lean)
42 'plugin/no-unsupported-browser-features': [
43 true,
44 {
45 severity: 'warning', // Warn but don't block - sometimes cutting-edge features are worth it
46 ignorePartialSupport: true, // Don't warn if feature has partial support
47 },
48 ],
49 },
50}

Create .browserslistrc:

1last 2 chrome version
2last 2 firefox version
3last 2 Edge version
4last 2 Safari version
5iOS >= 18
6not dead

This file tells the no-unsupported-browser-features plugin which browsers you're targeting, so it can warn you about CSS features that won't work in your supported browsers. The configuration above covers modern browsers while ensuring iOS 18+ compatibility for mobile users.

Why Stylelint Matters for CSS-in-JS

Performance Focus: The selector limits (selector-max-compound-selectors, selector-max-specificity) prevent performance-killing CSS that makes browsers work harder to match elements. Even with CSS-in-JS, badly structured CSS can slow down your app.

Bundle Size: Rules like at-rule-disallowed-list: ['import'] prevent render-blocking CSS imports, and declaration-no-important discourages CSS that's hard to optimize.

CSS-in-JS Compatibility: The disabled rules (selector-class-pattern, no-descending-specificity) acknowledge that CSS-in-JS tools like Emotion handle these concerns automatically through scoping and dynamic class generation.

Browser Support: The no-unsupported-browser-features plugin warns when you use CSS features that might need polyfills, helping you make informed decisions about bundle size vs. cutting-edge features.

5. Update Next.js Configuration

This is where the magic happens - configuring Next.js to work seamlessly with Emotion using SWC, Next.js's native Rust-based compiler (not Babel). SWC is significantly faster than Babel and comes built-in with Next.js, so we get excellent performance out of the box.

Update next.config.ts:

1import type { NextConfig } from 'next'
2
3const nextConfig: NextConfig = {
4 // Disable cache in development for faster iteration
5 ...(process.env.NODE_ENV !== 'production' && {
6 cacheMaxMemorySize: 0,
7 }),
8
9 // SWC compiler configuration for Emotion
10 compiler: {
11 emotion: {
12 // Source maps in development only - helps with debugging CSS-in-JS
13 sourceMap: process.env.NODE_ENV !== 'production',
14 // Auto-label components in development for easier debugging
15 autoLabel: process.env.NODE_ENV !== 'production' ? 'always' : 'never',
16 // Format for auto-generated labels
17 labelFormat: '[local]',
18 // Import map optimization - lets you import styled from @emotion/react
19 importMap: {
20 '@emotion/react': {
21 styled: {
22 canonicalImport: ['@emotion/styled', 'default'],
23 },
24 },
25 },
26 },
27 },
28
29 // Additional optimizations you might want to add:
30 experimental: {
31 // Enable optimizeCss for better CSS bundling (optional)
32 optimizeCss: true,
33 },
34}
35
36export default nextConfig

Why This Configuration Matters

SWC vs Babel: This config uses SWC (Speedy Web Compiler), Next.js's built-in Rust-based compiler, instead of Babel. SWC is up to 20x faster than Babel and requires zero additional setup. The compiler.emotion field tells SWC how to handle Emotion's CSS-in-JS transforms.

Development vs Production:

  • In development: Source maps and auto-labeling help you debug which components are generating which styles
  • In production: These features are disabled for smaller bundles and better performance

Import Map Optimization: The import map allows you to write import styled from '@emotion/react' instead of import styled from '@emotion/styled', which can be more convenient and consistent with other Emotion imports.

Cache Configuration: Disabling cache in development prevents stale build artifacts from causing confusion during development.

6. Set up Theme System

Create lib/theme.d.ts:

1import '@emotion/react'
2
3declare module '@emotion/react' {
4 export interface Theme {
5 colors: {
6 background: string
7 text: string
8 muted: string
9 floating: string
10 primary: string
11 }
12 }
13}

Create lib/theme.ts:

1import type { Theme } from '@emotion/react'
2
3export const colors: Theme = {
4 colors: {
5 background: 'hsl(0deg, 0%, 100%)',
6 text: 'hsl(230deg, 30%, 15%)',
7 muted: 'hsl(220deg, 20%, 85%)',
8 floating: 'hsl(0deg, 0%, 95%)',
9 primary: 'hsl(325deg, 90%, 72%)',
10 },
11}

7. Add CSS Reset

Create lib/reset.ts:

1import { css } from '@emotion/react'
2
3export const resetCss = css`
4 *,
5 *::before,
6 *::after {
7 box-sizing: border-box;
8 }
9
10 * {
11 margin: 0;
12 padding: 0;
13 }
14
15 body {
16 line-height: 1.5;
17 -webkit-font-smoothing: antialiased;
18 }
19
20 img,
21 picture,
22 video,
23 canvas,
24 svg {
25 display: block;
26 max-width: 100%;
27 }
28
29 input,
30 button,
31 textarea,
32 select {
33 padding: 0;
34 font: inherit;
35 color: inherit;
36 text-align: inherit;
37 text-transform: inherit;
38 vertical-align: middle;
39 background: transparent;
40 border-style: none;
41 }
42
43 li {
44 list-style: none;
45 }
46
47 p,
48 h1,
49 h2,
50 h3,
51 h4,
52 h5,
53 h6 {
54 overflow-wrap: break-word;
55 }
56
57 a {
58 color: inherit;
59 text-decoration: none;
60 }
61
62 #root,
63 #__next {
64 isolation: isolate;
65 }
66`

This reset gives you a clean foundation to build on, with sensible defaults that work well across different browsers.

Why This Stack?

This setup might seem like overkill for a simple project, but each piece serves a purpose:

  • Prettier eliminates formatting discussions and keeps code consistent
  • ESLint catches bugs early and enforces good practices
  • Emotion provides powerful styling capabilities with TypeScript support
  • Stylelint ensures your CSS-in-JS is performant and maintainable
  • The reset gives you a predictable starting point across browsers

The best part? Once you have this setup, you can copy these configs to any new Next.js project and be productive immediately.

Wrapping Up

That's my complete Next.js setup checklist. It takes about 15 minutes to get through all these steps, but it saves hours of debugging and configuration headaches later.

Feel free to adjust the configurations to match your preferences - the important thing is having a consistent starting point that you can rely on project after project.

Related Articles