How I’d Spin Up a NestJS Project in 2026 — Without the Usual Setup Errors

A pragmatic, engineering-first guide to resolving Node 24, pnpm 11, and TypeScript 7 tooling issues before writing your first line of application code.

#Engineering#Backend#NestJS#TypeScript#Architecture

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.ts
  • app.controller.spec.ts
  • app.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:

  1. Verify Runtimes:
node --version
pnpm --version
  1. Execute Nest CLI directly:
pnpm dlx @nestjs/cli@12.0.3 new my-api --package-manager pnpm --language ts --strict
  1. Pin Compatible TypeScript:
pnpm add -D typescript@^6
pnpm exec tsc --version
  1. Clean Dependencies:
pnpm install --force
  1. Verify Development Server:
pnpm run start:dev
  1. Confirm API Baseline:
  • Environment configuration (.env / .env.example)
  • Security headers (Helmet) & CORS
  • Global ValidationPipe
  • URI Versioning (/v1)
  • Functional /v1/health endpoint
  • 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 →