# Designing the Core Architecture

## **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:

```markdown
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:

```markdown
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.
