Starting a new backend project should be boring.
You should install the tools, create the project, run the dev server, and start building.
But in 2026, even a fresh NestJS project can run into problems before you write your first real line of application code.
That happened to me recently.
I was setting up a new NestJS backend with Node 24 and pnpm. The project itself wasn’t complicated. The problems came from the tooling around it: CLI resolution issues, package manager state desyncs, and a TypeScript version that was newer than what the Nest CLI could currently work with.
So instead of pretending the setup was completely straightforward, I decided to document how I would approach it now.
The goal isn’t to create the most sophisticated NestJS starter.
The goal is simpler: Get to a predictable, working NestJS foundation before building the actual application.
1. Start with your runtime and package manager
Before creating the project, establish the versions you’re actually working with.
For this setup, the baseline was:
- Node:
24.19.0 - pnpm:
11.22.0 - NestJS:
12.0.3 - Nest CLI:
12.0.3 - TypeScript:
^6
Check your local environment first:
node --version
pnpm --version
You don’t necessarily need these exact versions for every NestJS project. The important part is knowing what you’re running.
A lot of setup problems become difficult to diagnose when the developer doesn’t know whether an error is coming from Node, pnpm, Nest, TypeScript, or a globally cached package. Version awareness is the first step of setup.
2. Don’t assume the global Nest CLI is going to behave perfectly
The obvious approach is usually:
npm install -g @nestjs/cli
nest new my-api
That can work. But when you’re using pnpm and targeting a specific NestJS release, invoking the CLI explicitly through package execution is much cleaner.
For this project, we used:
pnpm dlx @nestjs/cli@12.0.3 new godere-api \
--package-manager pnpm \
--language ts \
--strict
This has an important advantage: You explicitly define which Nest CLI version generates the project. You aren’t relying on whatever version happens to be cached globally on your machine.
3. The Nest CLI and schematics can become a problem
One of the first issues I encountered was a CLI error where Nest couldn’t properly resolve its schematics collection.
The application hadn’t even been written yet, but the tooling failed.
When the command responsible for creating the project fails, don’t immediately start changing application configuration—there is no application code to debug yet. First, isolate and verify the tooling environment.
Verify your CLI resolution:
nest --version
If the global CLI behaves unpredictably, fall back to explicit execution:
pnpm dlx @nestjs/cli@12.0.3
For a fresh project, a reproducible execution command is always better than relying on an unverified global state.
4. Don’t blindly accept the newest TypeScript
This was the issue that cost the most time.
The fresh project scaffold resolved to TypeScript 7. At first glance, that sounds great—it’s the latest major release. But newer doesn’t automatically mean compatible.
When the Nest CLI attempted to run type-checking and AST transformations, it threw a compiler error:
The installed TypeScript version (7.0.x)
does not expose the programmatic compiler API
that the Nest CLI requires.
With TypeScript 7’s architectural shift toward a native Go-powered engine (“Project Corsa”), frameworks that depend on the classical JavaScript-based programmatic compiler API (import * as ts from 'typescript') can fail unless they hook into native bindings or pin back to the TypeScript 6 compiler line.
The solution was to pin back to TypeScript 6:
pnpm add -D typescript@^6
Then verify the local compiler binary:
pnpm exec tsc --version
Expected output: TypeScript 6.x.x
5. Why pinning versions matters
There is a common instinct in JavaScript development: just install the latest version.
That works fine until framework tools depend on specific compiler APIs.
Nest CLI relies on TypeScript compiler capabilities. TypeScript moves forward, and framework tooling has to catch up. A perfectly valid, cutting-edge TypeScript release can still break your framework’s build chain.
That’s why having this in your package.json:
{
"devDependencies": {
"typescript": "^6"
}
}
is far better than accepting whatever latest resolves to on the day you run the init command.
The lesson isn’t that new compiler major versions are bad. The lesson is: Use the TypeScript version that your framework tooling actively supports.
6. Verify TypeScript before doing anything serious
After updating your TypeScript dependency, don’t just assume the build is fixed.
First, check the compiler version:
pnpm exec tsc --version
Then boot the development server:
pnpm run start:dev
And verify the application compiles cleanly. When you resolve a tooling or dependency issue, verify that exact fix immediately before adding new code.
7. pnpm can leave you with a confusing installation state
Another issue I ran into was more subtle. At one point, running:
pnpm install
reported:
Already up to date
Yet the local node_modules state didn’t reflect the filesystem expectations.
When pnpm’s virtual store (.pnpm) or symlink state gets out of sync, inspect what is actually present on your disk:
ls node_modules
If dependencies are missing despite pnpm reporting a clean state, force a full re-link to reset the local store:
pnpm install --force
In my case, forcing the installation reset the symlink tree and restored the expected packages. When the package manager says everything is installed but the editor can’t resolve imports, check the filesystem before touching code.
8. Get a clean NestJS baseline before building features
Once the tooling works, resist the temptation to immediately start writing business logic.
First establish a clean baseline. A default NestJS template contains placeholder files you don’t need:
app.controller.tsapp.controller.spec.tsapp.service.ts
Remove the boilerplate so the repository reflects your real architecture.
For a backend API, your initial setup milestone should be intentionally simple:
Application starts
↓
Environment loads
↓
Configuration works
↓
Validation works
↓
Security middleware works
↓
API versioning works
↓
Health endpoint responds
If all of those work predictably, you have a foundation. Only then do you start building application features.
9. Establish environment configuration early
Don’t scatter process.env calls across your codebase. Centralize configuration from day one.
Set up your .env file:
NODE_ENV=development
PORT=3000
SUPABASE_URL=[https://your-project.supabase.co](https://your-project.supabase.co)
SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-key
SANITY_PROJECT_ID=your-project-id
SANITY_DATASET=production
SANITY_API_VERSION=2026-09-20
CORS_ORIGINS=http://localhost:3000
Keep secrets out of source control. Ensure your .gitignore includes .env, and commit a clean .env.example file for your team:
NODE_ENV=
PORT=
SUPABASE_URL=
SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
SANITY_PROJECT_ID=
SANITY_DATASET=
SANITY_API_VERSION=
CORS_ORIGINS=
This makes deployment to platforms like Render, Railway, or Fly.io seamless later on.
10. Configure the application globally
For an API, establish these core boundaries inside main.ts immediately:
Validation Pipe
Enforce global DTO validation:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
Security Headers & CORS
Apply Helmet middleware and explicit CORS origins rather than allowing open wildcards.
API Versioning
Enable URI versioning from day one:
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: "1",
});
Resulting in predictable endpoints:
/v1/health/v1/auth/me/v1/products/v1/orders
Retrofitting API versioning after clients are consuming unversioned routes is unnecessary pain.
11. Your first real endpoint should be boring
Start with a health endpoint: GET /v1/health
{
"status": "ok",
"service": "godere-api",
"version": "1",
"timestamp": "2026-09-20T22:00:00.000Z"
}
It doesn’t do anything complex, and that’s the point. It gives you a reliable way to verify server execution, container health checks, and deployment monitoring.
12. Don’t build your entire backend around generic folders
Avoid grouping your entire application into generic technical layers:
controllers/
services/
utils/
As the product grows, business domains matter far more than technical categories. Structure your application around feature modules:
src/
├── auth/
├── users/
├── businesses/
├── products/
├── offers/
├── inventory/
├── carts/
├── orders/
├── payments/
├── fulfillment/
├── deliveries/
├── notifications/
└── health/
Each domain module encapsulates its own controllers, services, DTOs, and repositories. This keeps the codebase readable and aligned with the domain model.
13. Treat the backend as a business-logic boundary
For anything beyond a simple CRUD app, the API owns the rules:
Customer ──> Catalog ──> Offers ──> Inventory ──> Cart ──> Order ──> Payment ──> Fulfillment
The frontend delivers the user experience. The backend enforces whether an operation is actually valid. Maintaining a strict boundary here makes it easier to support web apps, mobile clients, and third-party webhooks without duplicating business logic.
14. Keep external infrastructure behind internal interfaces
Your API will likely rely on third-party integrations:
- Database/Auth: Supabase
- CMS: Sanity
- Payments: Paystack, Flutterwave, Stripe
- Logistics: Local Dispatch, FEZ
Wrap third-party tools in your own internal providers:
Godere API
│
├── PaymentProvider
│ ├── Paystack
│ └── Flutterwave
│
├── FulfillmentProvider
│ ├── Local Dispatch
│ └── FEZ
│
└── NotificationProvider
└── WhatsApp / Email
Third-party SDKs are implementation details. Isolating them behind clear interfaces ensures provider changes won’t force refactors across your entire core domain.
15. Don’t start with microservices
When building authentication, commerce, payments, and logistics, it is tempting to break them into separate microservices immediately.
Start with a modular monolith:
- One API codebase.
- Clear domain module boundaries.
- Shared infrastructure where appropriate.
If a specific domain eventually needs independent scaling, its existing module boundary makes extracting it simple. Let actual scale dictate your system architecture.
16. The 2026 Setup Checklist
Before writing product features, complete this checklist:
- Verify Runtimes:
node --version
pnpm --version
- Execute Nest CLI directly:
pnpm dlx @nestjs/cli@12.0.3 new my-api --package-manager pnpm --language ts --strict
- Pin Compatible TypeScript:
pnpm add -D typescript@^6
pnpm exec tsc --version
- Clean Dependencies:
pnpm install --force
- Verify Development Server:
pnpm run start:dev
- Confirm API Baseline:
- Environment configuration (
.env/.env.example) - Security headers (Helmet) & CORS
- Global
ValidationPipe - URI Versioning (
/v1) - Functional
/v1/healthendpoint - Domain-driven module structure
17. The Core Takeaway
Don’t confuse newest with ready.
A framework release might depend on a specific compiler API. A new TypeScript release can modify that API. A package manager can report a clean install while local symlinks need resetting. A global CLI can conflict with what your project actually requires.
None of these are flaws in NestJS—they are standard realities of modern software tooling.
The solution is to make your build environment predictable:
- Know your runtime versions.
- Use explicit, compatible tooling.
- Pin compiler dependencies.
- Verify your fixes step-by-step.
- Establish a solid API foundation before building features.
Make the first stage of your project boring. If generating the project, installing dependencies, starting the server, and reaching /v1/health are all predictable, you’ve eliminated a huge class of problems before writing your core application logic.
Need operational clarity in your business?
We help founders and leadership teams build structured workflows, custom digital systems, and continuous tax governance that scale cleanly.
Start a Conversation →