Getting Started with Web App Development Using NestJS and React ─ Learning Project Structure and Configuration Management by Building a Blog Site
Introduction
In this series, we will gradually learn everything from the basics of web application development to operations, while developing a simple blog app using NestJS.
In the end, we will complete a blog app with a simple UI built in React that displays posted articles, and we will also cover a local development environment using Docker and deployment to a production environment using a managed DB such as Neon.
NestJS is a Node.js framework built on TypeScript. It has a module structure similar to Angular and allows for highly extensible and maintainable design.
Leveraging these characteristics, we will build a working application step by step.
What we will build in this series
- A blog post API using NestJS
- Type-safe DB operations using Prisma
- Environment-specific configuration management using multiple
.envfiles - An operations-conscious structure including logging, error handling, and testing
- Finally, display blog posts with React and deploy to production using a Docker setup
The final goal will look something like this:
By deploying using a cloud service called Railway, the article content entered in React will be saved in the database and displayed as a list.
Overview of the series and where this article fits
Series structure
- Getting Started with Web App Development Using NestJS ─ Learning Project Structure and Configuration Management by Building a Blog Site ←
This article - Let’s Build an Article Posting API with NestJS ─ Basics of Introducing Prisma and Implementing CRUD
- Improving Application Reliability ─ Logging, Error Handling, and Testing Strategy
- Deep Dive into Prisma and DB Design ─ Models, Relations, and Operational Design
- Build the UI with React and Deploy to Production ─ Docker and Environment Setup
In this article (Part 1), we will focus on setting up the NestJS project and building the “configuration management foundation” that will support future development.
We will carefully cover not only a simple “Hello World” but also .env management and directory design with an eye on the future structure.
The code implemented this time is stored in the following repository, so please refer to it together.
Target readers and what you will learn
This series is intended for people like the following:
- Those who are interested in NestJS and want to learn through a practical app
- Those who want to use modern development tools such as Prisma and Docker together
- Those who want to learn not just how to “get a web app running somehow,” but also design and operations
- Those who want to postpone React and the frontend for now and first build a solid foundation for the API and backend
We aim to start from the basics of NestJS and complete a working application with a configuration that is actually usable.
Overall project picture and technologies used
The application we will build in this series is a blog system centered on NestJS, incorporating a structure that takes into account the database, frontend, and even production operations.
While the theme is “simple article posting and display,” the goal is to naturally acquire design and operational know-how that can be used in real-world projects.
What is NestJS?
NestJS is a web application framework for Node.js built on TypeScript.
It has a module structure and DI (Dependency Injection) container inspired by Angular, and can handle everything from simple APIs to scalable microservices.
The main advantages of NestJS include:
- Clear architecture that is easy to extend
- TypeScript by default, enabling type-safe development
- High affinity with modern surrounding technologies such as Prisma, GraphQL, and Swagger
- Logging, exception handling, configuration management, and testing are easy to organize from the start
List of technologies used
| Item | Technology | Purpose |
|---|---|---|
| Framework | NestJS | API design and backend foundation |
| ORM | Prisma | DB schema management and type-safe operations |
| Database | PostgreSQL (Docker) | Local development DB |
| DB hosting | Neon / PlanetScale | Managed DB for production |
| Configuration management | @nestjs/config | Environment-specific configuration management via .env |
| Frontend | React (+ Vite) | UI for displaying blog posts |
| Testing | Jest + Supertest | Unit tests and E2E tests |
| Deployment | Docker / Render | Operation and publishing of development and production environments |
Architecture from development to production
The NestJS app will run directly on the local host environment and connect to PostgreSQL running in a Docker container for development.
Ultimately, we will serve the blog UI built with React via NestJS and deploy it as a Docker container to services such as Render. The DB will use an external service (Neon) to keep the structure extensible.
Local environment
Production environment
Creating a NestJS project
From here, we will actually spin up a NestJS project.
NestJS provides an official tool called Nest CLI, which allows you to create a project with an initial structure in just a few commands.
Using this CLI, directory structure and TypeScript settings are automatically set up, making it attractive for getting started smoothly.
Installing Nest CLI
First, install Nest CLI globally.
If you already have it installed, you can check the version with nest --version and skip this step.
npm install -g @nestjs/cli
-g means “global install.” This allows you to use the nest command from any project.
Creating a project
Next, use the CLI to create a new project.
This time, let’s create a project named nestjs-blog.
nest new nestjs-blog
You will then see an interactive prompt like this:
Which package manager would you ❤️ to use?
❯ npm
yarn
pnpm
- Use the arrow keys to select the one you usually use (e.g.,
npm) and press Enter. - The required dependency packages will then be installed automatically.
Entering the project
Move into the created directory.
cd nestjs-blog
Inside, the basic NestJS structure is already in place.
nestjs-blog/
├── src/
│ ├── app.controller.ts ← Routing (handling URLs)
│ ├── app.module.ts ← Defines the overall app structure
│ ├── app.service.ts ← Where you write business logic
│ └── main.ts ← Entry point (app startup process)
├── test/ ← Test code
├── package.json ← List of dependency packages
└── tsconfig.json ← TypeScript settings
Starting the app
Next, let’s start the NestJS app and check that it works.
We’ll start it in development mode with hot reload.
npm run start:dev
If it succeeds, you should see a message like this:
In this state, open your browser and access http://localhost:3000.
If the string “Hello World!” is displayed, your NestJS app has started successfully!🎉
Summary so far
- Using Nest CLI, you can quickly spin up a NestJS app without complex configuration
- TypeScript and directory structure are already set up
- If you see “Hello World!” in the browser, you’re ready to go!
In the next section, we’ll dig into environment variables and configuration management to refine the project so it can withstand future development.
Basic directory structure and module design
A NestJS project is characterized by a directory structure that emphasizes clear separation of responsibilities and extensibility.
At first glance, you might feel “there are a lot of files,” but each has a purpose, and once you understand and master them, you’ll have a very clean development experience.
In this chapter, we’ll unravel the initial structure and get a feel for NestJS’s design philosophy.
Understanding the standard NestJS structure
When you create a project with Nest CLI, the src/ directory has the following structure:
src/
├── app.controller.ts // Routing definitions
├── app.module.ts // Overall app structure (module registration)
├── app.service.ts // Business logic (core processing)
└── main.ts // Entry point (app startup)
Their roles are as follows:
| File | Role |
|---|---|
main.ts |
Starts the application (e.g., NestFactory.create()) |
app.module.ts |
Central module that aggregates the app. A hub that registers each feature |
app.controller.ts |
Entry point that receives HTTP requests (routing) |
app.service.ts |
Location for actual processing and business logic |
What is AppModule?
In NestJS, all features are organized in units called “modules.”
AppModule is positioned as the “root module (= entry point for the entire app)” and plays the following role:
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
imports: Import other modules here (e.g., DatabaseModule, PostsModule)controllers: Controllers (responsible for routing) that this module hasproviders: Classes that are injected via DI (dependency injection), such as services and utilities
All modules are ultimately registered in AppModule to be recognized by NestJS.
Responsibilities of Controller / Service
NestJS strictly separates responsibilities by clearly dividing Controller and Service, enforcing the Separation of Concerns.
| Role | Responsibility | Example implementation file |
|---|---|---|
| Controller | Routing and handling input | app.controller.ts |
| Service | Business logic and executing processing | app.service.ts |
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
Controllers are limited to “routing and parameter handling,” and the core processing is always delegated to Services. This provides the following benefits:
- Responsibilities are clear, making testing and maintenance easier
- Logic is not written in Controllers, so the code is easier to read
- Services can be reused (e.g., by multiple Controllers or event handlers)
Module-based structure as the default
In NestJS, it is recommended to separate features by “○○Module” units.
src/
├── posts/
│ ├── posts.module.ts
│ ├── posts.controller.ts
│ └── posts.service.ts
├── users/
│ ├── users.module.ts
│ └── ...
Organizing things this way helps maintain a codebase with good visibility even as the project grows.
Summary so far
- NestJS architecture is centered around “modules”
- AppModule is the root that manages and aggregates feature-specific modules
- Separating Controller and Service clarifies responsibilities, making testing and extension easier
Managing environment variables and separating configuration files
In modern web applications, it is fundamental not to “hard-code environment-dependent values (DB URLs, API keys, etc.) into the code.”
In NestJS, this can be achieved with .env files, but to handle them more effectively, it is recommended to introduce the @nestjs/config package.
In this section, we will explain how to manage environment variables with development, production, and test environments in mind, and how to use them.
Installing the @nestjs/config package
npm install @nestjs/config
This allows you to automatically load .env files and use a shared ConfigService throughout the app.
Creating a .env file
First, create a .env file in the project root and define environment variables like the following:
# .env (for development)
PORT=3000
DATABASE_URL=postgresql://bloguser:blogpass@localhost:5432/blogdb
It is convenient later to separate files by environment, such as
.env.productionor.env.test.local.
Loading the configuration module in AppModule
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
+ import { ConfigModule } from '@nestjs/config'
@Module({
- imports[],
+ imports: [
+ ConfigModule.forRoot({
+ isGlobal: true,
+ envFilePath: ['.env'],
+ }),
+ ],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
By setting isGlobal: true, you can use ConfigService without importing ConfigModule in every module.
Using ConfigService to retrieve values
Let’s check that environment variables are being retrieved correctly. (After confirming it works, you can delete the parts you added.)
+ import { ConfigService } from '@nestjs/config'
@Injectable()
export class AppService {
- getHello(): string {
- return 'Hello World!';
- }
+ constructor(private configService: ConfigService) {}
+ getPort(): string {
+ return this.configService.get<string>('PORT') || '3001';
+ }
}
By using configService.get, you can use the values of the loaded environment variables.
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
- return this.appService.getHello();
+ return this.appService.getPort();
}
}
Now, when you access http://localhost:3000/, the value of PORT (3000) will be displayed on the screen so you can verify it.
Switching .env files by environment
You will inevitably encounter situations where you want to switch settings depending on the execution environment, such as production, development, and test.
In NestJS, you can flexibly achieve this by switching .env files based on the value of the NODE_ENV environment variable.
ConfigModule.forRoot({
isGlobal: true,
envFilePath: [
`.env.${process.env.NODE_ENV}`,
'.env',
],
}),
With this, if you set NODE_ENV in the execution environment, the appropriate .env will be loaded automatically.
# Development environment
NODE_ENV=development npm run start:dev
# Production environment (after build)
NODE_ENV=production node dist/main.js
process.env.NODE_ENVis a standard Node.js environment variable. It can also be controlled in CI/CD and Docker.
Do not include .env files in Git (leverage NestJS defaults)
When you generate a NestJS project with Nest CLI, the .gitignore already includes the following .env-related settings:
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
This ensures that the following configuration files, which often contain sensitive information, are not committed to Git:
| File name | Example role |
|---|---|
| .env | Default development settings |
| .env.production.local | Production server-specific secret settings (DB URL, etc.) |
| .env.local | Local individual settings per developer (not shared within the team) |
Add .env.example to provide shared guidance
In actual operations, it is recommended to prepare a “template file” that is intended to be included in Git.
# .env.example
PORT=3000
DATABASE_URL=
This makes it clear “what environment variables are needed” when other developers clone the project.
Moving to a monorepo structure including the frontend
Here we will introduce how to refactor an existing NestJS backend project into a structure that can accommodate a future frontend. We will organize it into a simple monorepo structure with frontend/ and backend/ side by side.
By adopting this structure:
- The roles of frontend and backend become clear
- Docker and CI/CD settings are easier to organize
- Project extensibility improves
and other benefits can be gained.
New directory structure image
your-project-root/
├── backend/ # NestJS + Prisma
│ ├── src/
│ ├── prisma/
│ ├── .env
│ └── ...
├── frontend/ # Next.js or React (to be added later)
│ └── ...
├── docker/ # Shared docker configuration (as needed)
├── docker-compose.yml
└── README.md
Migration steps (grouping the backend into backend/)
Create the backend/ directory
mkdir backend
Move the contents of the NestJS project
rsync -av --progress ./ ./backend \
--exclude .git --exclude .vscode --exclude backend
After moving, delete unnecessary files
find . -maxdepth 1 ! -name '.' ! -name '..' ! -name '.git' ! -name '.vscode' ! -name 'backend' -exec rm -rf {} +
Finally, check
ls -a
You’re done if the top-level directory looks like this:
.
..
.git
.vscode
backend
Operation check
cd backend
npm run start:dev
In this state, if you access http://localhost:3000 in your browser and see the string “Hello World!”, the directory move of the NestJS app has been completed successfully.
Conclusion
Explicitly checking once that configuration values are being retrieved correctly is an important process in any development, not just NestJS. In particular, if you proceed with environment variables “assuming they are being retrieved,” they may later surface as mysterious bugs.
As in this article, temporarily displaying values such as PORT on the screen to verify them is a modest but reliable habit. At the same time, deciding whether to write configuration retrieval logic in the Controller or delegate it to the Service is also an important design decision to be aware of from the early stages.
Precisely because this is the early stage of development, carefully making small checks and design decisions leads to future peace of mind and quality. Rather than rushing ahead, let’s first solidify the foundation step by step.
Next time: Facing NestJS structure and design principles
The handling of environment variables and responsibility-conscious structure covered this time is only the starting line.
In team development, structural decisions such as “what should be written where” are what determine long-term quality and maintainability.
Next time, while organizing the standard NestJS directory structure and module design concepts, we will delve into practical topics such as:
- The role of AppModule and criteria for its composition
- Responsibilities and separation of design for Controller / Service / Module
- How to design an initial scalable structure with future expansion in mind
We will build a solid architectural foundation by giving reasons for each part of the structure and seeing how initial design decisions affect later development speed and team understanding.
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Let's Build an Article Posting API with NestJS ─ Basics of Introducing Prisma and Implementing CRUD
2025/04/12Improving the Reliability of a NestJS App ─ Logging, Error Handling, and Testing Strategy
2024/09/11Deepening DB Design with NestJS × Prisma ─ Models, Relations, and Operational Design
2024/09/12NestJS × React × Railway: Implementing a Blog UI and Deploying to Production
2024/10/25




