Creating a Full-Stack Application with Angular 19,
Express, Node.js, and MongoDB using pnpm
This comprehensive guide walks you through creating a basic full-stack application that
retrieves data from MongoDB and displays it in an Angular 19 web application. We'll be using
pnpm as our package manager and setting up a monorepo structure to organize our codebase
effectively.
Setting Up the Monorepo Structure
Let's start by creating a monorepo structure using pnpm to manage both our frontend and
backend applications.
Prerequisites
First, ensure you have Node.js installed:
node -v
If Node.js isn't installed, download it from the official website. Next, install pnpm globally:
# Using npm
npm install -g pnpm
# Or using Brew if available
brew install pnpm
Verify the installation:
pnpm -v
Creating the Monorepo
1. Create a root directory for your project:
mkdir mean-fullstack-app
cd mean-fullstack-app
2. Initialize pnpm in the root directory:
pnpm init
3. Create a pnpm workspace configuration file:
# Create pnpm-workspace.yaml file
echo "packages:
- 'apps/*'
- 'packages/*'" > pnpm-workspace.yaml
4. Set up the directory structure:
mkdir -p apps/server apps/client packages/tsconfig
This will create a structure like this:
mean-fullstack-app/
├── apps/
│ ├── client/
│ └── server/
├── packages/
│ └── tsconfig/
├── package.json
└── pnpm-workspace.yaml
This monorepo approach allows us to manage both the frontend and backend code within a
single repository while sharing common configurations and packages [1] .
Setting Up the Backend (Express.js and MongoDB)
Let's start with setting up the backend using Express.js and MongoDB.
Initialize the Server Project
1. Navigate to the server directory:
cd apps/server
2. Initialize a new package.json file:
pnpm init
3. Open the generated package.json and modify the main field to "src/index.js" [1] .
Install Backend Dependencies
pnpm add express mongoose cors dotenv
pnpm add -D typescript @types/express @types/node @types/cors ts-node nodemon
Configure TypeScript
1. Create a tsconfig.json file:
npx tsc --init
2. Update the tsconfig.json with appropriate settings:
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"outDir": "./dist",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
3. Create a src directory for your server code:
mkdir src
Create the Basic Express Server
Create a file src/index.ts:
import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
// MongoDB Connection
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/meanapp';
mongoose.connect(MONGODB_URI)
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('MongoDB connection error:', err));
// Basic route
app.get('/api', (req, res) => {
res.json({ message: 'Welcome to the MEAN Stack API' });
});
// Start server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Create MongoDB Model and Routes
1. Create a models directory:
mkdir src/models
2. Create a model for your data (e.g., src/models/item.ts):
import mongoose, { Schema, Document } from 'mongoose';
export interface IItem extends Document {
name: string;
description: string;
createdAt: Date;
}
const ItemSchema: Schema = new Schema({
name: { type: String, required: true },
description: { type: String, required: true },
createdAt: { type: Date, default: Date.now }
});
export default mongoose.model<IItem>('Item', ItemSchema);
3. Create a routes directory:
mkdir src/routes
4. Create routes for CRUD operations (e.g., src/routes/items.ts):
import express, { Router } from 'express';
import Item from '../models/item';
const router: Router = express.Router();
// Get all items
router.get('/', async (req, res) => {
try {
const items = await Item.find();
res.json(items);
} catch (err) {
res.status(500).json({ message: 'Server error' });
}
});
// Create a new item
router.post('/', async (req, res) => {
try {
const newItem = new Item({
name: req.body.name,
description: req.body.description
});
const savedItem = await newItem.save();
res.status(201).json(savedItem);
} catch (err) {
res.status(400).json({ message: 'Invalid data' });
}
});
export default router;
5. Update your src/index.ts to include the routes:
// ... other imports
import itemRoutes from './routes/items';
// ... middleware setup
// Routes
app.use('/api/items', itemRoutes);
// ... server startup
6. Add npm scripts to package.json:
"scripts": {
"dev": "nodemon src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
Create Environment Variables
Create a .env file in the server directory:
PORT=3000
MONGODB_URI=mongodb://localhost:27017/meanapp
This completes the backend setup. The server is now configured to connect to MongoDB and
provide API endpoints for items [2] [3] .
Setting Up the Angular 19 Frontend
Now, let's set up the Angular 19 frontend application.
Create a New Angular Project
1. Navigate to the client directory:
cd ../../apps/client
2. Install Angular CLI globally:
pnpm add -g @angular/cli
3. Create a new Angular application:
ng new client --standalone --routing --style=scss
cd client
Configure Proxy for API Communication
Create a proxy.conf.json file in the client directory:
{
"/api": {
"target": "http://localhost:3000",
"secure": false,
"logLevel": "debug"
}
}
Update the angular.json file to use the proxy configuration:
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"proxyConfig": "proxy.conf.json"
},
// ... other config
}
Create Service to Fetch Data
1. Generate a service to communicate with the API:
ng generate service services/item
2. Update the service (src/app/services/item.service.ts):
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface Item {
_id: string;
name: string;
description: string;
createdAt: Date;
}
@Injectable({
providedIn: 'root'
})
export class ItemService {
private apiUrl = '/api/items';
constructor(private http: HttpClient) { }
getItems(): Observable<Item[]> {
return this.http.get<Item[]>(this.apiUrl);
}
addItem(item: { name: string; description: string }): Observable<Item> {
return this.http.post<Item>(this.apiUrl, item);
}
}
Create Components to Display Data
1. Generate a component to display the items:
ng generate component components/item-list
2. Update the component (src/app/components/item-list/item-list.component.ts):
import { Component, OnInit } from '@angular/core';
import { ItemService, Item } from '../../services/item.service';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-item-list',
standalone: true,
imports: [CommonModule],
templateUrl: './item-list.component.html',
styleUrls: ['./item-list.component.scss']
})
export class ItemListComponent implements OnInit {
items: Item[] = [];
loading = true;
error: string | null = null;
constructor(private itemService: ItemService) { }
ngOnInit(): void {
this.fetchItems();
}
fetchItems(): void {
this.itemService.getItems().subscribe({
next: (data) => {
this.items = data;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load items';
this.loading = false;
console.error(err);
}
});
}
}
3. Update the template file (src/app/components/item-list/item-list.component.html):
<div class="container">
<h2>Items List</h2>
<div *ngIf="loading">Loading...</div>
<div *ngIf="error" class="error">{{ error }}</div>
<div *ngIf="!loading && !error">
<div *ngIf="items.length === 0">No items found.</div>
<div *ngIf="items.length > 0" class="items-grid">
<div *ngFor="let item of items" class="item-card">
<h3>{{ item.name }}</h3>
<p>{{ item.description }}</p>
<small>Created: {{ item.createdAt | date }}</small>
</div>
</div>
</div>
</div>
4. Add some basic styling (src/app/components/item-list/item-list.component.scss):
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.item-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.error {
color: red;
font-weight: bold;
}
Update the App Module to Include HTTP Client
Update src/app/app.config.ts to include HttpClientModule:
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient()
]
};
Update the App Routes
Update src/app/app.routes.ts to include the ItemListComponent:
import { Routes } from '@angular/router';
import { ItemListComponent } from './components/item-list/item-list.component';
export const routes: Routes = [
{ path: '', redirectTo: 'items', pathMatch: 'full' },
{ path: 'items', component: ItemListComponent }
];
Update App Component
Update src/app/app.component.html:
<header>
<h1>MEAN Stack Application</h1>
</header>
<main>
<router-outlet></router-outlet>
</main>
<footer>
<p>© 2025 MEAN Stack Demo</p>
</footer>
This completes the Angular frontend setup [4] [5] .
Creating a Form to Add New Items
Let's enhance our application by adding a form to create new items.
Create an Item Form Component
1. Generate a new component:
ng generate component components/item-form
2. Update the component (src/app/components/item-form/item-form.component.ts):
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { ItemService } from '../../services/item.service';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-item-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './item-form.component.html',
styleUrls: ['./item-form.component.scss']
})
export class ItemFormComponent {
itemForm: FormGroup;
submitting = false;
error: string | null = null;
success = false;
constructor(
private fb: FormBuilder,
private itemService: ItemService
) {
this.itemForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
description: ['', [Validators.required, Validators.minLength(5)]]
});
}
onSubmit(): void {
if (this.itemForm.invalid) {
return;
}
this.submitting = true;
this.error = null;
this.success = false;
this.itemService.addItem(this.itemForm.value).subscribe({
next: () => {
this.submitting = false;
this.success = true;
this.itemForm.reset();
},
error: (err) => {
this.submitting = false;
this.error = 'Failed to add item';
console.error(err);
}
});
}
}
3. Create the template (src/app/components/item-form/item-form.component.html):
<div class="form-container">
<h2>Add New Item</h2>
<form [formGroup]="itemForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" formControlName="name">
<div *ngIf="itemForm.get('name')?.invalid && itemForm.get('name')?.touched" class="
Name is required and must be at least 3 characters long.
</div>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" formControlName="description" rows="4"></textarea>
<div *ngIf="itemForm.get('description')?.invalid && itemForm.get('description')?.to
Description is required and must be at least 5 characters long.
</div>
</div>
<button type="submit" [disabled]="itemForm.invalid || submitting">
{{ submitting ? 'Adding...' : 'Add Item' }}
</button>
</form>
<div *ngIf="error" class="error-message">{{ error }}</div>
<div *ngIf="success" class="success-message">Item added successfully!</div>
</div>
4. Add some styling (src/app/components/item-form/item-form.component.scss):
.form-container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.error {
color: red;
font-size: 12px;
margin-top: 5px;
}
.error-message {
color: red;
margin-top: 20px;
}
.success-message {
color: green;
margin-top: 20px;
}
5. Update src/app/app.routes.ts to include the form component:
import { Routes } from '@angular/router';
import { ItemListComponent } from './components/item-list/item-list.component';
import { ItemFormComponent } from './components/item-form/item-form.component';
export const routes: Routes = [
{ path: '', redirectTo: 'items', pathMatch: 'full' },
{ path: 'items', component: ItemListComponent },
{ path: 'add-item', component: ItemFormComponent }
];
6. Update the app component (src/app/app.component.html) to include navigation:
<header>
<h1>MEAN Stack Application</h1>
<nav>
<a routerLink="/items">View Items</a>
<a routerLink="/add-item">Add Item</a>
</nav>
</header>
<main>
<router-outlet></router-outlet>
</main>
<footer>
<p>© 2025 MEAN Stack Demo</p>
</footer>
Setting Up Scripts for the Entire Project
Let's update the root package.json to include scripts that run both the frontend and backend:
{
"name": "mean-fullstack-app",
"version": "1.0.0",
"scripts": {
"start:server": "cd apps/server && pnpm run dev",
"start:client": "cd apps/client && ng serve",
"dev": "concurrently \"pnpm run start:server\" \"pnpm run start:client\"",
"build:server": "cd apps/server && pnpm run build",
"build:client": "cd apps/client && ng build",
"build": "pnpm run build:server && pnpm run build:client"
},
"devDependencies": {
"concurrently": "^8.2.0"
}
}
Install the concurrently package:
pnpm add -D concurrently
Running the Application
1. Start MongoDB:
If using MongoDB locally, make sure your MongoDB server is running.
2. From the root directory, start both the frontend and backend:
pnpm run dev
This will concurrently run the Express.js backend server and the Angular frontend development
server.
3. Open your browser and navigate to:
Frontend: http://localhost:4200
Backend API: http://localhost:3000/api
Conclusion
You've successfully created a full-stack MEAN application using pnpm as the package
manager. The application:
1. Uses a monorepo structure to organize frontend and backend code
2. Has a Node.js/Express.js backend connected to MongoDB
3. Features an Angular 19 frontend that fetches and displays data from the API
4. Includes a form to add new items to the database
This basic application provides a foundation that you can extend with more features, such as
authentication, additional CRUD operations, and more complex data models. The monorepo
approach makes it easier to share code between frontend and backend, improving
maintainability and development workflow.
By following these steps, you've created a modern full-stack JavaScript application that
demonstrates the power and flexibility of the MEAN stack with the efficiency of pnpm for
package management.
⁂
Understanding the Missing app.module.ts in
Modern Angular Projects
If you've noticed that the app.module.ts file is missing from your Angular project's src/app folder,
you're encountering one of the significant changes introduced in recent versions of Angular. This
isn't an error – it's by design in newer Angular applications.
Why app.module.ts is Missing
Starting from Angular v17 (released in November 2023), the Angular team made "Standalone
Components" the new default for applications created with the Angular CLI. This means:
New Angular projects no longer generate the traditional app.module.ts file automatically
The application structure now relies on standalone components that don't require NgModule
declarations
This change was implemented to simplify Angular development by reducing boilerplate
code [6] [7]
Your Options for Handling This Situation
Option 1: Embrace Standalone Components (Recommended)
The Angular team strongly recommends using standalone components as they are easier to
understand, require less boilerplate, and represent the future direction of Angular development.
With standalone components, you directly import what you need in each component rather than
declaring everything in modules [6] .
Option 2: Create a New Project with NgModules
If you prefer or require the traditional NgModule approach (perhaps for compatibility with
existing code or tutorials):
ng new your-project-name --no-standalone
This command will generate a new project with the traditional module-based structure, including
app.module.ts [7] [8] .
Option 3: Manually Add app.module.ts to Your Existing Project
If you need to add an app.module.ts file to your existing standalone project:
1. Create a new file named app.module.ts in your src/app directory
2. Add the following basic structure (customize as needed) [6] :
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
// Add other components here
],
imports: [
BrowserModule,
// Add other modules here
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
3. Update your main.ts file to use this module instead of the standalone bootstrap
Understanding Angular's Evolution
This change reflects Angular's evolution toward a simpler component model. The standalone
approach allows developers to:
Directly import dependencies where they're needed
Avoid the cognitive overhead of managing NgModules
Benefit from better tree-shaking for smaller bundle sizes
Create more portable, self-contained components [6] [9]
Conclusion
The missing app.module.ts file is not an issue but a reflection of Angular's move toward a more
streamlined development experience. You can either adapt to this new approach (recommended
for new projects) or use the --no-standalone flag when creating new projects if you prefer the
traditional module-based structure.
If you're following older tutorials or courses that reference app.module.ts, keep in mind they were
likely created before this change in Angular 17, and you'll need to make appropriate adjustments
or create a non-standalone project.
⁂
1. https://ayoubkhial.com/blog/mean-web-app-part-2-express-meets-typescript
2. https://github.com/DavideViolante/Angular-Full-Stack
3. https://www.mongodb.com/resources/languages/mongodb-and-npm-tutorial
4. https://www.youtube.com/watch?v=jJAkjtXLyLw
5. https://www.youtube.com/watch?v=gYkxU1eBEYk
6. https://stackoverflow.com/questions/77454741/why-doesnt-app-module-exist-in-angular-17
7. https://www.linkedin.com/pulse/how-fix-missing-appmodulets-file-latest-version-angular-sofia-nayak-
er0df
8. https://www.youtube.com/watch?v=k9GjQenY7xw
9. https://www.telerik.com/blogs/best-both-angular-worlds-standalone-modules-combined