Step-by-step guide to set up a production-ready Next.js project for 2025. Every step is optional and can be skipped if not needed or swapped for a different tool depending on the project requirements.
Selected tools:
- Next.js with TypeScript, using App Router
- Tailwind CSS for styling
- Biome for linting and formatting
- Husky and lint-staged for pre-commit hooks
- GitHub Actions for CI
- Vitest and React Testing Library for unit and integration tests
- Playwright for E2E tests
- Storybook for component development and documentation
npx create-next-app@latestSelect preferred options and wait for the installation to finish. I've selected TypeScript, Tailwind CSS, and no ESLint (see the next step). Also, I've selected App Router which provides support for RSC.
We will use Biome, which is an alternative to Eslint and Prettier in one package. It is significantly faster than Eslint and Prettier, and it is simpler to configure.
- https://biomejs.dev/
- Install Biome:
npm install --save-dev --save-exact @biomejs/biome - Initialize Biome:
npx @biomejs/biome init(createsbiome.json) - Edit configuration in
biome.json, as needed - Set up your IDE to use Biome for linting and formatting, i.e. Jetbrains
Biomeplugin - Edit linting and formatting scripts in
package.json:"scripts": { ... "lint": "npx @biomejs/biome lint --write ./src", // lint and apply safe fixes "check": "npx @biomejs/biome check ./src" // lint and format check (intended for CI) }
We will use Husky and lint-staged to set up pre-commit hooks.
- Install Husky :
npm install --save-dev husky - Husky init (adds prepare script):
npx husky init - Install lint-staged:
npm install --save-dev lint-staged - Update
.husky/pre-committo runlint-staged:npx lint-staged
- Update
package.jsonto includelint-stagedconfiguration (see biome docs):"lint-staged": { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [ "biome check --files-ignore-unknown=true" ] }
We will use GitHub Actions to check the code on every push: linting and formatting, testing, security checks, and building the app.
See .github/workflows/ci.yml for the configuration.
See .github/PULL_REQUEST_TEMPLATE.md for the default PR template.
We will use Vitest and React Testing Library as our main tools for testing.
For more information check the Next.js documentation
- Install Vitest dependencies:
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom vite-tsconfig-paths - Add
vitest.config.mts - Add
testscript topackage.json:"scripts": { ... "test": "vitest" }
- Write your tests in
__tests__directory or alongside the component files (with.test.ts(x)extension).
For more information check the Next.js documentation
- To install Playwright, run
npm init playwrightoryarn create playwright. - Go through the setup process and select the options that suit your project.
- It can also create a GitHub Actions workflow for you.
- Set baseURL to
http://localhost:3000inplaywright.config.ts. - Uncomment
webServerinplaywright.config.tsand setcommandtonpm run dev. It will start the development server before running the tests. - Update Vitest config to exclude e2e tests. Make sure to keep the default
excludevalue! Seevitest.config.mtsfor the configuration.
We will use Storybook to develop components in isolation and document them.
- Init Storybook:
npx storybook@latest init. This will:- Add
.storybookdirectory with the configuration - Add
storybookscripts topackage.json - Add dependencies and configuration relevant to Next.js
- Add
src/storiesexample directory
- Add
- Add
sbshortcut topackage.json - Rename script
build-storybooktostorybook:build - Check the Next.js section in the Storybook documentation for more information about limitations and supported features.
Tailwind CSS is used as a default for styling. shadcn/ui is used for easier creation and styling of basic components.
Styled JSX is also available by default in both Next.js and Storybook, so it can be used in special cases where it might be more appropriate than Tailwind.
Added by default in the Next.js template. You can customize it by editing tailwind.config.js.
We will use shadcn/ui to build our component library. It is a collection of
components built with TailwindCSS. Great advantage is that it provides independent
components you can copy-paste and use in your project, with maximum customization.
- Init
shadcn/ui:npx shadcn@latest init -d- If using
npm, it will ask you to use either--forceor--legacy-peer-deps. - If using
yarn,bunorpnpm, it will install the package without any additional steps. - See shadcn/ui documentation for more information.
- If using
- Init step will:
- Add following dependencies:
tailwind-mergeandtailwind-animatelucide-react(icons)class-variance-authority(for class variance)clsx(for classnames)
- Add
components.jsonfile which holds configuration for your project - Update
tailwind.config.jswith the new configuration - Update
src/app/global.csswith the new CSS variables- You can choose to use either CSS variables or Tailwind utility classes for theming
- We will use CSS variables in this project
- Add
cnutility tosrc/lib/utils.ts(we will later move this to another place)
- Add following dependencies:
Theme is managed through CSS variables defined in app/globals.css, which are
exposed to the Tailwind through tailwind.config.js.
We use a simple background and foreground convention for colors. The
background variable is used for the background color of the component and the
foreground variable is used for the text color.
The
-backgroundsuffix is omitted when the variable is used for the background color of the component, we only explicitly use-foregroundsuffix.
NOTE: This is slightly different from the default
shadcn/uitheme, update as needed.
background - default background color (i.e. <body /> and similar)
foreground - default text color
foreground-secondary - muted text color on a primary background. Currently derived from foreground, with opacity applied.
title - title color
muted - muted background color
muted-foreground - text color on muted background
card and popover - card and popover background color, currently the same as muted
accent, accent-foreground - used for accents such as hover effects on Ghost Button, , ...etc
primary - primary button background color
primary-foreground - primary button text color
secondary - secondary button background color
secondary-foreground - secondary button text color
destructive - used for destructive actions such as <Button variant="destructive">
destructive-foreground - destructive button text color
border - default border color
input - border color for inputs such as <Input />, <Select />, <Textarea />
ring - focus ring color
We will structure the project in a way that is easy to maintain over time. It is based around well-defined modules with clear interfaces and responsibilities.
src/components- library of reusable (generic) components. This should be treated almost as an external library, as it should be possible to extract it as a separate package in the future.src/features- verticals of the application. Every feature folder should contain domain specific code for a given feature. Features could be application specific or generic.src/app- Next.js app root. It should consume features and components to create the final application.src/utils- generic utility functionssrc/hooks- generic hookssrc/api- pre-configured API clientssrc/types- TypeScript types (top level, like environment variables, window, etc.)src/assets- static assetssrc/stories- Storybook entry point (individual stories can be placed alongside components)
Structure of a feature folder should be as follows, but feel free to adapt this based on the needs and complexity of the feature (start simple and evolve toward this):
+-- api # exported API request declarations and api hooks related to a specific feature
|
+-- assets # assets folder can contain all the static files for a specific feature
|
+-- components # components scoped to a specific feature
|
+-- hooks # hooks scoped to a specific feature
|
+-- routes # route components for a specific feature pages
|
+-- stores # state stores for a specific feature
|
+-- types # typescript types for TS specific feature domain
|
+-- utils # utility functions for a specific feature
|
+-- index.ts
IMPORTANT: To maintain modularity, everything from a feature should be exported from the index.ts file which behaves as the public API of the feature!
You should import stuff from other features only by using:
import {FeatureComponent} from "@/features/some-feature"and not
import {FeatureComponent} from "@/features/some-feature/components/FeatureComponent"- There is not yet a feature parity with ESLint's
no-restricted-imports. Keep an eye on the Biome documentation for updates. - We want equivalent to this:
"no-restricted-imports": [
"error",
{
"patterns": ["@/features/*/*", "@/components/*/*"]
}
],