DEV Community

Cover image for Setting Up Your Next.js App the Right Way
Aman Kumar Singh
Aman Kumar Singh

Posted on • Originally published at amanksingh.com

Setting Up Your Next.js App the Right Way

There's a version of "setting up Next.js" that takes thirty seconds: run create-next-app, done. And for a weekend project, that's genuinely all you need. But we're building a SaaS meant to live for years, inside a monorepo, sharing types with a backend. The choices you make in the first hour here quietly shape how the next hundred hours feel.

So this isn't a "run this command" article. It's the handful of setup decisions that actually matter, and the ones people agonize over that don't.

This is part three of the Full Stack SaaS Masterclass, continuing from the Turborepo monorepo we set up last time. The Next.js app lives in apps/web.

App Router or Pages Router?

This is the first fork, and I'll give you a straight answer: for a new project in 2023, use the App Router. The Pages Router isn't bad. It's stable, and a lot of great apps run on it. But the App Router is where React and Next are clearly heading, and starting a multi-year project on the path that's being deprioritized is borrowing trouble.

The App Router's real advantage for a SaaS is Server Components. Your dashboard can fetch data on the server, render it to HTML, and ship less JavaScript to the browser. For an app with a lot of data-heavy pages, that's faster loads and better SEO on your marketing pages, more or less for free.

There's a learning curve. Server Components versus Client Components trips everyone up at first. I'll cover that properly in the React series later. For now: it's worth the curve.

The setup that matters

Inside apps/web, here's what I actually configure before writing any features.

TypeScript, strict. Non-negotiable. Turn on strict mode in tsconfig.json from the very first file. Retrofitting strictness onto a codebase that grew up loose is miserable. I've done it: it's weeks of any whack-a-mole. Pay the small tax now.

Path aliases. Set up @/ to point at your source root so imports stay readable as the app grows.

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The difference this makes six months in:

// without aliases, from a deeply nested file
import { Button } from '../../../../components/ui/Button';

// with aliases, from anywhere
import { Button } from '@/components/ui/Button';
Enter fullscreen mode Exit fullscreen mode

That second version doesn't break when you move the file. The first one does, and you won't notice until runtime.

Consume the shared types. This is the monorepo paying off. Import the User type from the shared package instead of redefining it:

import type { User } from '@my-saas/types';

async function getProfile(): Promise<User> {
  const res = await fetch(`${process.env.API_URL}/me`);
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

Now if the backend changes the User shape, this file won't compile until you deal with it. That's the safety net we built the monorepo for, doing its job on the frontend.

Tailwind for styling. An opinion, but a well-worn one. For a SaaS you'll build a lot of custom UI, and Tailwind lets you style without inventing a naming system or context-switching to CSS files. If you or your team genuinely prefer CSS Modules or styled-components, that's a fine choice too. This is one of those reversible decisions not worth a war over.

A sane folder structure

People burn absurd amounts of time on folder structure. Here's one that works and that you can stop thinking about:

apps/web/src/
├── app/              # App Router routes
├── components/
│   ├── ui/           # dumb, reusable primitives (Button, Input)
│   └── features/     # feature-specific components
├── lib/              # helpers, API client, utilities
├── hooks/            # custom React hooks
└── styles/           # global css, tailwind entry
Enter fullscreen mode Exit fullscreen mode

The one distinction I'd insist on is ui versus features. ui holds generic primitives that know nothing about your app. A Button doesn't care whether it's on a billing page. features holds components that do: a BillingSummary that knows about invoices. Keeping those separate stops your reusable primitives from slowly getting tangled up in business logic, which is how component libraries rot.

Everything else, don't overthink. You can always move a file. You cannot always undo a strict: false codebase.

What not to install yet

The instinct on day one is to add every library you might need. Resist it. You don't yet know if you need a state management library. Server Components and React's built-in state cover more than they used to. You don't need a data-fetching library until you have data to fetch. You don't need a form library until you have a form.

Each dependency is a thing to learn, update, and occasionally get burned by. Add them when a real need shows up, not in anticipation. The create-next-app default plus TypeScript, Tailwind, and your shared types is a complete enough starting point to build real features on.

Next in the series, we set up the other side: the NestJS backend that serves those shared types, inside the same monorepo.

Key takeaways

  • For a new multi-year project, start on the App Router. It's where Next.js is heading, and Server Components cut shipped JavaScript.
  • Turn on TypeScript strict from the first file; retrofitting it later is painful.
  • Set up @/ path aliases so imports stay stable when you move files.
  • Import shared types from the monorepo package so frontend and backend can't drift.
  • Keep reusable ui primitives separate from features components, and don't install libraries until a real need appears.

FAQ

Should I use the App Router or Pages Router for a new Next.js app?
Use the App Router for new projects. It's the direction Next.js is investing in, and Server Components reduce client-side JavaScript and improve SEO. The Pages Router still works but is the legacy path.

Why enable TypeScript strict mode from the start?
Strict mode catches whole categories of bugs at compile time. Adding it to an existing loose codebase means fixing a flood of errors all at once, so it's far cheaper to start strict.

What folder structure should a Next.js SaaS use?
A simple one: app for routes, components/ui for generic primitives, components/features for app-specific components, plus lib, hooks, and styles. Keep reusable primitives free of business logic.

Do I need a state management library like Redux?
Not to start. React's built-in state and Server Components handle a lot now. Add a state library only when you hit genuine shared-state complexity that local state can't handle cleanly.

How do I share types between my Next.js app and backend?
Put the types in a shared monorepo package and import them on both sides. Any change to a type then forces both apps to update, preventing silent mismatches.

Further reading


About the Author

Hi, I'm Aman Singh — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.

I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.


Top comments (0)