Skip to main content

Command Palette

Search for a command to run...

Designing the Core Architecture

How I structured the foundation of the NestJS MVP StarterKit

Updated
5 min read
Designing the Core Architecture
I

Graduated from Telkom University with a degree in Computer Science, specializing in software development and systems design. Recently contributed to Supernova Palapa Nusantara as a Software Engineer, where efforts focused on developing and implementing software solutions, improving project reliability, and collaborating with multidisciplinary teams. Key competencies include software architecture, quality assurance, and backend programming.

At Supernova Palapa Nusantara, contributed to software architecture enhancements with a focus on system reliability and project goals. Previous roles involved optimizing and developing web platforms, such as university systems, in collaboration with cross-functional teams. Brings a collaborative approach and a commitment to delivering impactful digital solutions that align with organizational objectives.

Introduction

After deciding to build my own NestJS StarterKit, the first challenge was to design a core architecture that actually deserves to be called a “starter.” Not just a bunch of boilerplate folders, but a structure that could grow with real-world projects, from early MVPs to production-grade systems.

At my last job, I worked at a software house that delivered a lot of client projects. Each new project started with the same conversation:

“Should we copy the last repo again?”

That’s the problem I want to solve, to create a starter that’s modular, maintainable, and easy to extend. This article walks through how I designed the core architecture of that StarterKit.

Architectural Goals

Before touching any line of code, I wrote down a few non-negotiables for the architecture:

  1. Fast to start — a new MVP should be bootstrapped in minutes.

  2. Modular and scalable — each domain should live independently, but still play nicely with others.

  3. Developer-friendly — clear conventions, minimal surprises.

  4. No vendor lock-in — I want to own the stack, not depend on third-party auth or config systems.

  5. Extendable without refactor hell — adding a feature shouldn’t break existing modules.

These goals shape every decision in the StarterKit, from folder naming to dependency design.

Core Design Principles

There are three big ideas behind the architecture:

  • Domain-driven modular structure
    Each domain (like user, auth, post, etc.) lives as its own NestJS module — fully self-contained, with its own entities, DTOs, and services.

  • Convention over configuration
    The StarterKit uses opinionated patterns (like where to put guards, filters, or repositories) so developers spend less time deciding and more time building.

  • DIY-first, vendor-second
    Instead of relying on services like Clerk or Auth0, I implement our own auth, config, and persistence layers, flexible and portable.

Folder & Module Structure

Here’s the project skeleton at a glance:

src/
├── common/
│   ├── dtos/
│   ├── interceptors/
│   ├── helpers/
│   └── ...
├── config/
│   ├── app.config.ts
│   └── ...
├── lib/
│   ├── prisma/
│   └── ...
├── modules/
│   ├── auth/
│   ├── user/
│   └── ...
├── app.module.ts
└── main.ts

common/ folder holds global concerns and reusable building blocks that are not tied to any specific domain. Think of it as your project’s toolbox.
config/ all configuration logic lives here, not scattered in .env calls across the app.
lib/ folder is for wrapping third-party libraries or SDKs so they integrate cleanly into Nest’s DI ecosystem.
modules/ contains all business domains. Each one can live independently or be plugged into other projects.

This separation helps maintain domain isolation: the auth module shouldn’t need to know about user, and vice versa. It also simplifies testing, you can load only the module you’re working on.

Key Architectural Components

Here’s what makes up the “core” of the StarterKit:

1. Config Module

Centralized environment management, with schema validation.
Everything reads from a single source of truth, no magic process.env calls scattered around.

2. Prisma Module

Instead of wrapping Prisma behind a custom repository layer, the StarterKit uses Prisma directly through a dedicated PrismaModule located under lib/prisma.

This keeps things simple and closer to how Prisma is designed to be used, strongly typed, auto-generated, and efficient.
Every domain service can inject PrismaService directly for database access, benefiting from full TypeScript type safety and autocomplete on every query.

3. Common Utilities

Contains:

  • Global interceptors and filters (logging, error handling)

  • Custom pipes and decorators

  • Shared exceptions and response wrappers

They keep the app consistent and predictable across modules.

4. Modules — the heart of the StarterKit

Every business domain in the StarterKit lives inside its own NestJS module under the modules/ directory.
This is where the actual business logic resides, the features that make your app what it is.

A typical domain module looks like this:

src/modules/user/
├── user.controller.ts
├── user.service.ts
├── user.module.ts
└── dto/
    ├── create-user.dto.ts
    └── update-user.dto.ts

Each module is self-contained and follows the same conventions:

  • Controller handles HTTP requests, validation, and routing.

  • Service contains the domain logic and interacts with Prisma or external services.

  • DTOs define the input/output contracts.

  • Module file (*.module.ts) wires everything together and exports what other modules may need.

The philosophy is simple:

“Each domain owns its data, logic, and contracts — no shared global state.”

This modular layout makes it easy to:

  • Add or remove features without breaking unrelated parts of the system.

  • Scale horizontally — you can later extract a module into its own microservice if needed.

  • Keep team boundaries clear — each developer or squad can own a module independently.

5. AppModule as Composition Root

AppModule ties everything together.
It imports core modules, registers global providers, and bootstraps the dependency graph.

Design Alternatives I Considered

I actually explored three different paths before landing here:

  1. Monolithic structure
    ✅ Simple to start, but scales poorly.
    ❌ Changes in one area can ripple everywhere.

  2. Microservices
    ✅ Great isolation, clear boundaries.
    ❌ Overkill for early MVPs; too much infra overhead.

  3. Modular monolith (chosen)
    ✅ Keeps isolation and simplicity.
    ✅ Easy to scale horizontally later.
    ❌ Requires clear boundaries discipline, but that’s a good thing.

In terms of architecture pattern, I leaned toward a Clean Architecture style, but not dogmatically, just enough layering to keep things tidy without abstraction fatigue.

Developer Experience

Architecture isn’t just about structure; it’s about how it feels to build with it.

  • Built-in CLI scripts to scaffold modules quickly.

  • Opinionated ESLint, Prettier, and commit hooks.

  • Strict TypeScript setup for safer refactors.

  • Auto-registration for common NestJS decorators.

The goal: new developers can clone, pnpm start:dev, and start shipping features, no setup pain.

What’s Next

The next step after architecture is to tackle Authentication.
In the next article “Building the Auth Module” . I’ll explain how I implemented a flexible auth flow with JWT, refresh tokens, and optional social login, all built on top of this core architecture.

Closing Thoughts

Good architecture isn’t about complexity, it’s about clarity.
When the system’s design reflects how you think about your problem domain, everything else becomes easier: refactoring, testing, even onboarding new devs.

This StarterKit is still evolving, but its foundation feels solid, simple enough for small projects, yet robust enough to grow into something much bigger.

More from this blog