Getting Started with Web App Development Using NestJS and React ─ Learning Project Structure and Configuration Management by Building a Blog Site

  • nestjs
    nestjs
Published on 2025/04/11

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 .env files
  • 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.

Image from Gyazo

Overview of the series and where this article fits

Series structure

  1. Getting Started with Web App Development Using NestJS ─ Learning Project Structure and Configuration Management by Building a Blog Site ← This article
  2. Let’s Build an Article Posting API with NestJS ─ Basics of Introducing Prisma and Implementing CRUD
  3. Improving Application Reliability ─ Logging, Error Handling, and Testing Strategy
  4. Deep Dive into Prisma and DB Design ─ Models, Relations, and Operational Design
  5. 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.

https://shinagawa-web.com/en/blogs/nestjs-blog-series-crud-and-prisma-intro

https://shinagawa-web.com/en/blogs/nestjs-blog-series-logging-error-testing

https://shinagawa-web.com/en/blogs/nestjs-blog-series-prisma-db-design

https://shinagawa-web.com/en/blogs/nestjs-blog-series-react-deploy

The code implemented this time is stored in the following repository, so please refer to it together.

https://github.com/shinagawa-web/nestjs-blog

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

Image from Gyazo

Production environment

Image from Gyazo

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.

Image from Gyazo

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:

Image from Gyazo

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:

src/app.module.ts
@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 has
  • providers: 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
src/app.controller.ts
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}
src/app.service.ts
@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.production or .env.test.local.

Loading the configuration module in AppModule

src/app.module.ts
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.)

app.service.ts
+ 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.

src/app.controller.ts
@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.

src/app.module.ts
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_ENV is 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.

https://shinagawa-web.com/en/blogs/nestjs-blog-series-crud-and-prisma-intro

Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form