Blog
{Blog}  See what’s new with MongoDB 6.0 — and why you’ll want to upgrade today >>

How to Use MEAN Stack: Build a Web Application from Scratch

What is the MEAN stack?

MEAN is a technology stack used for building full stack applications. It's a combination of the following technologies:

  • MongoDB—document database
  • Express—a Node.js framework for building APIs
  • Angular—front-end application framework
  • Node.js—server-side JavaScript runtime environment

Applications built with the MEAN stack follow the client-server architecture. The client, built with Angular, can be a web application, a native mobile application, or a desktop application. The client communicates with the server through an API, which is built with Express. The server then manages the requests with the MongoDB database.

Client-Server Architecture

Client-Server Architecture

What will this tutorial cover?

In this tutorial, we'll build a RESTful API that implements the CRUD (Create, Read, Update, Delete) operations for an employee. For data persistence, we'll be using a MongoDB Atlas cluster.

We'll be building an employee management web application. The interface will have the following pages:

  • View all employees
  • Add new employees
  • Update existing employees

Here's what our finished application looks like:

gif of angular app demonstration

Getting started

You'll need Node.js and a MongoDB Atlas cluster to follow this tutorial.

  1. Visit https://nodejs.org/ to download and install the current version of Node.js. This tutorial is tested with Node.js version 16.13.0. To make sure you're using the correct Node.js version, execute the following command in your terminal:
 node --version
  1. Follow the Get Started with Atlas guide to set up your free MongoDB Atlas cluster. Make sure you complete all the steps and locate your cluster's connection string. You'll need it later when we're connecting to the database.

Building the server-side Node.js and Express application

Let's start by creating a directory that will host our project and its files. We'll name it ‘mean-stack-example’.

mkdir mean-stack-example
cd mean-stack-example

We'll be using shell commands to create directories and files throughout the tutorial. However, you're more than welcome to use any other method.


Now, let's create the directories and files that will host our server-side application—‘server’ and ‘server/src’—and also initialize a ‘package.json’ file for it.

mkdir server && mkdir server/src
cd server
npm init -y


touch tsconfig.json .env
(cd src && touch database.ts employee.routes.ts employee.ts server.ts)

Installing dependencies

We'll need a few external packages to build the RESTful API and to connect to our MongoDB Atlas cluster. Let's install them using the npm install command.

npm install cors dotenv express mongodb

We'll also be using TypeScript for our server application. We'll install the TypeScript compiler and the supporting @types packages as development dependencies using the --save-dev flag. These packages are only used during development and shouldn't be included in the final production application.

npm install --save-dev typescript @types/cors @types/express @types/node ts-node

Finally, we'll paste the following into the tsconfig.json configuration file that TypeScript will use to compile our code.

mean-stack-example/server/tsconfig.json

{
   "compilerOptions": {
       "module": "commonjs",
       "esModuleInterop": true,
       "target": "es6",
       "noImplicitAny": true,
       "moduleResolution": "node",
       "sourceMap": true,
       "outDir": "dist",
       "baseUrl": ".",
       "allowJs": true,
       "paths": {
           "*": ["node_modules/*"]
       }
   },
   "include": ["src/**/*"]
}

Create an employee interface on the server side

Since we’re building an employee management app, the main data unit is the employee. Let's create an Employee interface that will be used to define the structure of the employee object.

mean-stack-example/server/src/employee.ts

import * as mongodb from "mongodb";
 
export interface Employee {
   name: string;
   position: string;
   level: "junior" | "mid" | "senior";
   _id?: mongodb.ObjectId;
}

Our employees should have a name, position, and level. The _id field is optional because it's generated by MongoDB. So, when we're creating a new employee, we don't need to specify it. However, when we get an employee object from the database, it will have the _id field populated.

Connect to the database

Let's implement the following function to connect to our database:

mean-stack-example/server/src/database.ts

import * as mongodb from "mongodb";
import { Employee } from "./employee";
 
export const collections: {
   employees?: mongodb.Collection<Employee>;
} = {};
 
export async function connectToDatabase(uri: string) {
   const client = new mongodb.MongoClient(uri);
   await client.connect();
 
   const db = client.db("meanStackExample");
   await applySchemaValidation(db);
 
   const employeesCollection = db.collection<Employee>("employees");
   collections.employees = employeesCollection;
}
 
// Update our existing collection with JSON schema validation so we know our documents will always match the shape of our Employee model, even if added elsewhere.
// For more information about schema validation, see this blog series: https://www.mongodb.com/blog/post/json-schema-validation--locking-down-your-model-the-smart-way
async function applySchemaValidation(db: mongodb.Db) {
   const jsonSchema = {
       $jsonSchema: {
           bsonType: "object",
           required: ["name", "position", "level"],
           additionalProperties: false,
           properties: {
               _id: {},
               name: {
                   bsonType: "string",
                   description: "'name' is required and is a string",
               },
               position: {
                   bsonType: "string",
                   description: "'position' is required and is a string",
                   minLength: 5
               },
               level: {
                   bsonType: "string",
                   description: "'level' is required and is one of 'junior', 'mid', or 'senior'",
                   enum: ["junior", "mid", "senior"],
               },
           },
       },
   };
 
   // Try applying the modification to the collection, if the collection doesn't exist, create it
  await db.command({
       collMod: "employees",
       validator: jsonSchema
   }).catch(async (error: mongodb.MongoServerError) => {
       if (error.codeName === 'NamespaceNotFound') {
           await db.createCollection("employees", {validator: jsonSchema});
       }
   });
}

We're using the MongoDB Node.js Driver distributed as the mongodb NPM package. First, we create a new MongoClient object with the provided connection string. Then, we connect to the database. We need to use await since connect is an asynchronous function. After that, we get the db object from the client object. We use this object to get the employees collection. Note that the variable employees is cast to the Employee interface. This will provide type checking to any query we send to the database. Finally, we assign the employees collection to the collections object which is exported from this file. That way, we can access the employees collection from other files such as the employee.routes.ts file which will implement our RESTful API.

We're also using JSON schema validation to ensure that all of our documents follow the shape of our Employee interface. This is a good practice to ensure that we don't accidentally store data that doesn't match the shape of our model.

We'll be persisting our data to a MongoDB Atlas cluster. To connect to our cluster, we'll need to set an ATLAS_URI environment variable that contains the connection string. Add it to the server/.env file. Make sure you replace the username and password placeholder with your credentials.

mean-stack-example/server/.env

ATLAS_URI=mongodb+srv://<username>:<password>@sandbox.jadwj.mongodb.net/meanStackExample?retryWrites=true&w=majority

Now, we can load the environment variables using the dotenv package. We can do that in any file but it's a good practice to do it as early as possible. Since the server.ts file will be our entry point, let's load the environment variables from it, connect to the database, and start the server:

mean-stack-example/server/src/server.ts

import * as dotenv from "dotenv";
import cors from "cors";
import express from "express";
import { connectToDatabase } from "./database";
 
// Load environment variables from the .env file, where the ATLAS_URI is configured
dotenv.config();
 
const { ATLAS_URI } = process.env;
 
if (!ATLAS_URI) {
   console.error("No ATLAS_URI environment variable has been defined in config.env");
   process.exit(1);
}
 
connectToDatabase(ATLAS_URI)
   .then(() => {
       const app = express();
       app.use(cors());
 
       // start the Express server
       app.listen(5200, () => {
           console.log(`Server running at http://localhost:5200...`);
       });
 
   })
   .catch(error => console.error(error));

Let's run the app and see if everything works:

npx ts-node src/server.ts

You should see the following output:

Server running at http://localhost:5200...

Good job! Now, we’re ready to build the RESTful API for our employees.

Build the RESTful API

In this section, we'll implement a ‘GET’, ‘POST’, ‘PUT’, and ‘DELETE’ endpoint for our employees. As you can notice, these HTTP methods correspond to the CRUD operations— Create, Read, Update, and Delete—we'll be performing on our employees in the database. The only caveat is that we'll have two ‘GET’ endpoints: one for getting all employees and one for getting a single employee by ID.

To implement the endpoints, we'll use the router provided by Express. The file we'll be working in is src/employee.routes.ts.

GET /employees

Let's start by implementing the ‘GET /employees’ endpoint which will allow us to get all the employees in the database.

mean-stack-example/server/src/employee.routes.ts

import * as express from "express";
import * as mongodb from "mongodb";
import { collections } from "./database";
 
export const employeeRouter = express.Router();
employeeRouter.use(express.json());
 
employeeRouter.get("/", async (_req, res) => {
   try {
       const employees = await collections.employees.find({}).toArray();
       res.status(200).send(employees);
   } catch (error) {
       res.status(500).send(error.message);
   }
});

We're using the find() method. Because we're passing in an empty object—{}—we'll get all the employees in the database. We'll then use the toArray() method to convert the cursor to an array. Finally, we'll send the array of employees to the client.

You may notice that the route is ‘/’. This is because we'll register all endpoints from this file under the ‘/employees’ route.

GET /employees/:id

Next, we'll implement the ‘GET /employees/:id’ endpoint which will allow us to get a single employee by ID. Append the following to the bottom of employee.routes.ts:

mean-stack-example/server/src/employee.routes.ts

employeeRouter.get("/:id", async (req, res) => {
   try {
       const id = req?.params?.id;
       const query = { _id: new mongodb.ObjectId(id) };
       const employee = await collections.employees.findOne(query);
 
       if (employee) {
           res.status(200).send(employee);
       } else {
           res.status(404).send(`Failed to find an employee: ID ${id}`);
       }
 
   } catch (error) {
       res.status(404).send(`Failed to find an employee: ID ${req?.params?.id}`);
   }
});

The ID of the employee is provided as a parameter. We use the ObjectId method to convert the string ID to a MongoDB ObjectId object. We then use the findOne() method to find the employee with the given ID. If the employee is found, we'll send it to the client. Otherwise, we'll send a ‘404 Not Found’ error.

If you're wondering what the ‘?’ symbol represents in some of the expressions above, it's optional chaining operator. It enables you to read the value of a nested property without throwing an error if the property doesn't exist. Instead of throwing an error, the expression evaluates to undefined.

POST /employees

The ‘POST /employees’ endpoint will allow us to create a new employee. Append the following to the bottom of employee.routes.ts:

mean-stack-example/server/src/employee.routes.ts

employeeRouter.post("/", async (req, res) => {
   try {
       const employee = req.body;
       const result = await collections.employees.insertOne(employee);
 
       if (result.acknowledged) {
           res.status(201).send(`Created a new employee: ID ${result.insertedId}.`);
       } else {
           res.status(500).send("Failed to create a new employee.");
       }
   } catch (error) {
       console.error(error);
       res.status(400).send(error.message);
   }
});

We receive the employee object from the client in the request body. We'll use the insertOne() method to insert the employee into the database. If the insertion is successful, we'll send a ‘201 Created’ response with the ID of the employee. Otherwise, we'll send a ‘500 Internal Server Error’ response.

PUT /employees/:id

The ‘PUT /employees/:id’ endpoint will allow us to update an existing employee. Append the following to the bottom of 'employee.routes.ts':

mean-stack-example/server/src/employee.routes.ts

employeeRouter.put("/:id", async (req, res) => {
   try {
       const id = req?.params?.id;
       const employee = req.body;
       const query = { _id: new mongodb.ObjectId(id) };
       const result = await collections.employees.updateOne(query, { $set: employee });
 
       if (result && result.matchedCount) {
           res.status(200).send(`Updated an employee: ID ${id}.`);
       } else if (!result.matchedCount) {
           res.status(404).send(`Failed to find an employee: ID ${id}`);
       } else {
           res.status(304).send(`Failed to update an employee: ID ${id}`);
       }
   } catch (error) {
       console.error(error.message);
       res.status(400).send(error.message);
   }
});

Here, the ID of the employee is provided as a parameter whereas the employee object is provided in the request body. We use the ObjectId method to convert the string ID to a MongoDB ObjectId object. We then use the updateOne() method to update the employee with the given ID. If the update is successful, we'll send a ‘200 OK’ response. Otherwise, we'll send a ‘304 Not Modified’ response.

DELETE /employees/:id

Finally, the ‘DELETE /employees/:id’ endpoint will allow us to delete an existing employee. Append the following to the bottom of ‘employee.routes.ts’:

mean-stack-example/server/src/employee.routes.ts

employeeRouter.delete("/:id", async (req, res) => {
   try {
       const id = req?.params?.id;
       const query = { _id: new mongodb.ObjectId(id) };
       const result = await collections.employees.deleteOne(query);
 
       if (result && result.deletedCount) {
           res.status(202).send(`Removed an employee: ID ${id}`);
       } else if (!result) {
           res.status(400).send(`Failed to remove an employee: ID ${id}`);
       } else if (!result.deletedCount) {
           res.status(404).send(`Failed to find an employee: ID ${id}`);
       }
   } catch (error) {
       console.error(error.message);
       res.status(400).send(error.message);
   }
});

Similar to the previous endpoints, we send a query to the database based on the ID passed as a parameter. We use the deleteOne() method to delete the employee. If the deletion is successful, we'll send a ‘202 Accepted’ response. Otherwise, we'll send a ‘400 Bad Request’ response. If the employee is not found (result.deletedCount is 0), we'll send a ‘404 Not Found’ response.

Register the routes

Now, we need to instruct the Express server to use the routes we've defined. First, import the employeesRouter at the beginning of ‘src/server.ts’:

mean-stack-example/server/src/server.ts

import { employeeRouter } from "./employee.routes";

Then, add the following right before the app.listen() call:

mean-stack-example/server/src/server.ts

app.use("/employees", employeeRouter);

Finally, restart the server by stopping the shell process with Ctrl-C, and running it again.

npx ts-node src/server.ts

You should see the same message as before in the console.

Server running at http://localhost:5200...

Well done! We've built a simple RESTful API for our employees. Now, let's build an Angular web application to interact with it!

Building the client-side Angular web application

The next step is to build a client-side Angular web application that will interact with our RESTful API. We'll use the Angular CLI to scaffold the application. To install it, open a new terminal tab and run the following command in the terminal from the root of your mean-stack-example folder:

npm install -g @angular/cli

It's important to keep the server running while you're working on the client-side application. To do this, you'll need to open a new tab in your terminal where you will execute commands for the client-side application. Also, make sure you navigate to the mean-stack-example directory.


After the installation is finished, navigate to the root directory of the project and run the following command to scaffold a new Angular application:

ng new client --routing --style=css --minimal

This will create a new Angular application in the ‘client’ directory. The --routing flag will generate a routing module. The --style=css flag will enable the CSS preprocessor. The --minimal flag will skip any testing configuration as we won't be covering that in the tutorial. Installing the dependencies may take a while. After the installation is finished, navigate to the new application and start it by running the following commands:

cd client
ng serve -o

After the application is built, you should see a new tab in your browser window with the application running.


Newly Created Angular Web Application


Finally, for styling we'll use Bootstrap. Add the following to <head> section in the ‘src/index.html’ file:

mean-stack-example/client/src/index.html

<head>
 <meta charset="utf-8">
 <title>Client</title>
 <base href="/">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <link rel="icon" type="image/x-icon" href="favicon.ico">
 
 <!-- Add the next 5 lines -->
 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
   integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
 <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
   integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
   crossorigin="anonymous"></script>
</head>

Create an employee interface on the client-side

Similar to our server-side application, we'll create an Angular interface for our employees. We'll use the Employee interface to define the properties of our employee objects. Open a new terminal window and run the following command to scaffold the interface:

ng generate interface employee

Then, open the newly create ‘src/app/employee.ts’ file in your editor and add the following properties:

mean-stack-example/client/src/app/employee.ts

export interface Employee {
   name?: string;
   position?: string;
   level?: 'junior' | 'mid' | 'senior';
   _id?: string;
}

Creating an employee service

Angular recommends separating your business logic from your presentation logic. That's why we'll create a service that handles all communication with the ‘/employee’ endpoint of the API. The service will be used by the components in the application. To generate the service, run the following command:

ng generate service employee

The ‘ng generate service’ command creates a new service in the ‘src/app/employee.service.ts’ file. Replace the content of this file with the following:

mean-stack-example/client/src/app/employee.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, Subject, tap } from 'rxjs';
import { Employee } from './employee';
 
@Injectable({
 providedIn: 'root'
})
export class EmployeeService {
 private url = 'http://localhost:5200';
 private employees$: Subject<Employee[]> = new Subject();
 
 constructor(private httpClient: HttpClient) { }
 
 private refreshEmployees() {
   this.httpClient.get<Employee[]>(`${this.url}/employees`)
     .subscribe(employees => {
       this.employees$.next(employees);
     });
 }
 
 getEmployees(): Subject<Employee[]> {
   this.refreshEmployees();
   return this.employees$;
 }
 
 getEmployee(id: string): Observable<Employee> {
   return this.httpClient.get<Employee>(`${this.url}/employees/${id}`);
 }
 
 createEmployee(employee: Employee): Observable<string> {
   return this.httpClient.post(`${this.url}/employees`, employee, { responseType: 'text' });
 }
 
 updateEmployee(id: string, employee: Employee): Observable<string> {
   return this.httpClient.put(`${this.url}/employees/${id}`, employee, { responseType: 'text' });
 }
 
 deleteEmployee(id: string): Observable<string> {
   return this.httpClient.delete(`${this.url}/employees/${id}`, { responseType: 'text' });
 }
}

We're using the HttpClient service to make HTTP requests to our API. The refreshEmployees() method is used to fetch the full list of employees.

The HttpClient service is provided by Angular through the HttpClientModule. It's not part of the application by default—we need to import it in the ‘app.module.ts’ file. First, add the following import to the top of the ‘app.module.ts’ file:

mean-stack-example/client/src/app/app.module.ts

import { HttpClientModule } from '@angular/common/http';

Then, add the module to the list of imports of the AppModule class:

@NgModule({
 declarations: [
   AppComponent
 ],
 imports: [
   BrowserModule,
   AppRoutingModule,
   HttpClientModule
 ],
 providers: [],
 bootstrap: [AppComponent]
})
export class AppModule { }

Next, we'll create a new page that displays a table with our employees.

Create an employees list component

Let's create a new page for our table with employees. In Angular, a component is a reusable piece of code that can be used to display a view. We'll create a new component called EmployeesList and then also register it as the ‘/employees’ route in the application.

To generate the component, run the following command:

ng generate component employees-list

The Angular CLI generated a new component named EmployeesList in the ‘src/app/employees-list.component.ts’ file. It also declared the component in the ‘AppModule’ so we don’t have to do it manually. Replace the content of ‘src/app/employees-list.component.ts’ with the following:

mean-stack-example/client/src/app/employees-list/employees-list.component.ts

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Employee } from '../employee';
import { EmployeeService } from '../employee.service';
 
@Component({
 selector: 'app-employees-list',
 template: `
   <h2 class="text-center m-5">Employees List</h2>
 
   <table class="table table-striped table-bordered">
       <thead>
           <tr>
               <th>Name</th>
               <th>Position</th>
               <th>Level</th>
               <th>Action</th>
           </tr>
       </thead>
 
       <tbody>
           <tr *ngFor="let employee of employees$ | async">
               <td>{{employee.name}}</td>
               <td>{{employee.position}}</td>
               <td>{{employee.level}}</td>
               <td>
                   <button class="btn btn-primary me-1" [routerLink]="['edit/', employee._id]">Edit</button>
                   <button class="btn btn-danger" (click)="deleteEmployee(employee._id || '')">Delete</button>
               </td>
           </tr>
       </tbody>
   </table>
 
   <button class="btn btn-primary mt-3" [routerLink]="['new']">Add a New Employee</button>
 `
})
export class EmployeesListComponent implements OnInit {
 employees$: Observable<Employee[]> = new Observable();
 
 constructor(private employeesService: EmployeeService) { }
 
 ngOnInit(): void {
   this.fetchEmployees();
 }
 
 deleteEmployee(id: string): void {
   this.employeesService.deleteEmployee(id).subscribe({
     next: () => this.fetchEmployees()
   });
 }
 
 private fetchEmployees(): void {
   this.employees$ = this.employeesService.getEmployees();
 }
}

As you can notice, EmployeesListComponent is a TypeScript class that is decorated with the @Component decorator. The @Component decorator is used to indicate that this class is a component. The selector property is used to specify the HTML tag that will be used to display this component. Spoiler alert: We won't use this selector at all. Instead, we'll register the component as a route. The template property is used to specify the HTML template that will be used to display this component.

The implementation of the class contains the logic for the component. The ngOnInit() method is called when the component is rendered on the page. It's a good place to fetch the list of employees. For that, we're using the EmployeeService we created earlier. The getEmployees() method returns an observable. We subscribe to it with the async pipe in the template. This will automatically render the list of employees as soon as the data is available. We're using a table to display the employees together with a few Bootstrap classes that make it look nicer.

There are also a few actions in the template—for editing, deleting, and adding new employees. The [routerLink] attribute is used to navigate to the ‘/employees/edit/:id’ route, which we'll implement later in the tutorial. The (click) event is used to call the deleteEmployee() method. This method, implemented in the class, uses the EmployeeService to delete an employee.

Now that we've created our component, we need to register it as a route in the ‘app.routing.module.ts’ file. Replace the contents of the file with the following:

mean-stack-example/client/src/app/app.routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { EmployeesListComponent } from './employees-list/employees-list.component';
 
const routes: Routes = [
 { path: '', redirectTo: 'employees', pathMatch: 'full' },
 { path: 'employees', component: EmployeesListComponent },
];
 
@NgModule({
 imports: [RouterModule.forRoot(routes)],
 exports: [RouterModule]
})
export class AppRoutingModule { }

Then, go to the ‘src/app/app.component.ts’ file and remove the extraneous content from the template. Make sure you leave only the <router-outlet> tag, wrapped in a <div> tag.

mean-stack-example/client/src/app/app.component.ts

import { Component } from '@angular/core';
 
@Component({
 selector: 'app-root',
 template: `
 <div class="container-md">
   <router-outlet></router-outlet>
 </div>
 `
})
export class AppComponent { }

Let's refresh the browser and see if everything is working.

Employees list table


We have a table but we don't see any employees yet. Let's create a new page for adding employees.

Creating a page for adding employees

We need a form for filling in name, position, and level, to create a new employee. For editing existing employees, we'll need a similar form. Let's create an EmployeeForm component and reuse it for both adding and editing.

ng generate component employee-form -m app

Let's start by importing the ReactiveFormsModule in our AppModule:

mean-stack-example/client/src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
 
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { EmployeesListComponent } from './employees-list/employees-list.component';
import { EmployeeFormComponent } from './employee-form/employee-form.component';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms'; // <-- add this line
 
@NgModule({
 declarations: [
   AppComponent,
   EmployeesListComponent,
   EmployeeFormComponent
 ],
 imports: [
   BrowserModule,
   AppRoutingModule,
   HttpClientModule,
   ReactiveFormsModule // <-- add this line
 ],
 providers: [],
 bootstrap: [AppComponent]
})
export class AppModule { }

Now, we can use Angular's FormBuilder to create a reactive form:

mean-stack-example/client/src/app/employee-form/employee-form.component.ts

import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { BehaviorSubject } from 'rxjs';
import { Employee } from '../employee';
 
@Component({
 selector: 'app-employee-form',
 template: `
   <form class="employee-form" autocomplete="off" [formGroup]="employeeForm" (ngSubmit)="submitForm()">
     <div class="form-floating mb-3">
       <input class="form-control" type="text" id="name" formControlName="name" placeholder="Name" required>
       <label for="name">Name</label>
     </div>
 
     <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert alert-danger">
       <div *ngIf="name.errors?.['required']">
         Name is required.
       </div>
       <div *ngIf="name.errors?.['minlength']">
         Name must be at least 3 characters long.
       </div>
     </div>
 
     <div class="form-floating mb-3">
       <input class="form-control" type="text" formControlName="position" placeholder="Position" required>
       <label for="position">Position</label>
     </div>
 
     <div *ngIf="position.invalid && (position.dirty || position.touched)" class="alert alert-danger">
 
       <div *ngIf="position.errors?.['required']">
         Position is required.
       </div>
       <div *ngIf="position.errors?.['minlength']">
         Position must be at least 5 characters long.
       </div>
     </div>
 
     <div class="mb-3">
       <div class="form-check">
         <input class="form-check-input" type="radio" formControlName="level" name="level" id="level-junior" value="junior" required>
         <label class="form-check-label" for="level-junior">Junior</label>
       </div>
       <div class="form-check">
         <input class="form-check-input" type="radio" formControlName="level" name="level" id="level-mid" value="mid">
         <label class="form-check-label" for="level-mid">Mid</label>
       </div>
       <div class="form-check">
         <input class="form-check-input" type="radio" formControlName="level" name="level" id="level-senior"
           value="senior">
         <label class="form-check-label" for="level-senior">Senior</label>
       </div>
     </div>
 
     <button class="btn btn-primary" type="submit" [disabled]="employeeForm.invalid">Add</button>
   </form>
 `,
 styles: [
   `.employee-form {
     max-width: 560px;
     margin-left: auto;
     margin-right: auto;
   }`
 ]
})
export class EmployeeFormComponent implements OnInit {
 @Input()
 initialState: BehaviorSubject<Employee> = new BehaviorSubject({});
 
 @Output()
 formValuesChanged = new EventEmitter<Employee>();
 
 @Output()
 formSubmitted = new EventEmitter<Employee>();
 
 employeeForm: FormGroup = new FormGroup({});
 
 constructor(private fb: FormBuilder) { }
 
 get name() { return this.employeeForm.get('name')!; }
 get position() { return this.employeeForm.get('position')!; }
 get level() { return this.employeeForm.get('level')!; }
 
 ngOnInit() {
   this.initialState.subscribe(employee => {
     this.employeeForm = this.fb.group({
       name: [ employee.name, [Validators.required] ],
       position: [ employee.position, [ Validators.required, Validators.minLength(5) ] ],
       level: [ employee.level, [Validators.required] ]
     });
   });
 
   this.employeeForm.valueChanges.subscribe((val) => { this.formValuesChanged.emit(val); });
 }
 
 submitForm() {
   this.formSubmitted.emit(this.employeeForm.value);
 }
}

There's a lot of code here but there isn't anything groundbreaking to it. We're just using the FormBuilder to create a reactive form with three fields and we're also adding validation to the form. The template is displaying the error messages in case there's a validation error. We're using an @Input() to pass in the initial state of the form from the parent component. The type of the @Input() is BehaviorSubject<Employee> because we might pass async data into the form.

For example, the parent component might fetch the employee data from an API and pass it into the form. The child component will get notified when new data is available. The @Output() is an event emitter that will emit the form values whenever the form is submitted. The parent will handle the submission and send an API call.

The next step is to implement the AddEmployeeComponent:

ng generate component add-employee -m app

Replace the content of the newly created file with the following:

mean-stack-example/client/src/app/add-employee/add-employee.component.ts

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Employee } from '../employee';
import { EmployeeService } from '../employee.service';
 
@Component({
 selector: 'app-add-employee',
 template: `
   <h2 class="text-center m-5">Add a New Employee</h2>
   <app-employee-form (formSubmitted)="addEmployee($event)"></app-employee-form>
 `
})
export class AddEmployeeComponent {
 constructor(
   private router: Router,
   private employeeService: EmployeeService
 ) { }
 
 addEmployee(employee: Employee) {
   this.employeeService.createEmployee(employee)
     .subscribe({
       next: () => {
         this.router.navigate(['/employees']);
       },
       error: (error) => {
         alert("Failed to create employee");
         console.error(error);
       }
     });
 }
}

We're using the EmployeeForm, and whenever the AddEmployeeComponent receives a form submission, it will call the EmployeeService to create the employee. The EmployeeService will emit an event when the employee is created and the AddEmployeeComponent will navigate back to the table of employees.

While we're at it, let's implement the component for editing an employee:

ng generate component edit-employee -m app

Replace the content of the newly created file with the following:

mean-stack-example/client/src/app/edit-employee/edit-employee.component.ts

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { Employee } from '../employee';
import { EmployeeService } from '../employee.service';
 
@Component({
 selector: 'app-edit-employee.component.ts',
 template: `
   <h2 class="text-center m-5">Edit an Employee</h2>
   <app-employee-form [initialState]="employee" (formSubmitted)="editEmployee($event)"></app-employee-form>
 `
})
export class EditEmployeeComponent implements OnInit {
 employee: BehaviorSubject<Employee> = new BehaviorSubject({});
 
 constructor(
   private router: Router,
   private route: ActivatedRoute,
   private employeeService: EmployeeService,
 ) { }
 
 ngOnInit() {
   const id = this.route.snapshot.paramMap.get('id');
   if (!id) {
     alert('No id provided');
   }
 
   this.employeeService.getEmployee(id !).subscribe((employee) => {
     this.employee.next(employee);
   });
 }
 
 editEmployee(employee: Employee) {
   this.employeeService.updateEmployee(this.employee.value._id || '', employee)
     .subscribe({
       next: () => {
         this.router.navigate(['/employees']);
       },
       error: (error) => {
         alert('Failed to update employee');
         console.error(error);
       }
     })
 }
}

The only notable difference from the AddEmployeeComponent is that we're getting the employee ID from the URL, fetching the employee from the API, and then passing it to the form in the ngOnInit() method.


Finally, let's add navigation to our new pages:

mean-stack-example/client/src/app/app-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { EmployeesListComponent } from './employees-list/employees-list.component';
import { AddEmployeeComponent } from './add-employee/add-employee.component'; // <-- add this line
import { EditEmployeeComponent } from './edit-employee/edit-employee.component'; // <-- add this line
 
const routes: Routes = [
 { path: '', redirectTo: 'employees', pathMatch: 'full' },
 { path: 'employees', component: EmployeesListComponent },
 { path: 'employees/new', component: AddEmployeeComponent }, // <-- add this line
 { path: 'employees/edit/:id', component: EditEmployeeComponent }]; // <-- add this line
 
@NgModule({
 imports: [RouterModule.forRoot(routes)],
 exports: [RouterModule]
})
export class AppRoutingModule { }

Alright! Let's test it out! Go ahead and try to create a new employee. After filling in the details, click the Submit button. You should see the new employee on the list. Then, you can try editing it by clicking the Edit button. You should see the form filled with the employee's details. When you submit the form, the employee will be updated in the table. Finally, you can delete it by clicking the Delete button.


gif of angular app demonstration


If something doesn't add up, you can check out the finished project in the mean-stack-example repository.

Conclusion

Thank you for joining me on this journey and following along! I hope you enjoyed it and learned a lot. The MEAN stack makes it easy to get started and to build scalable applications. You're using the same language throughout the stack: JavaScript. Additionally, MongoDB's document model makes it natural to map data to objects and MongoDB Atlas's forever-free cluster makes it easy to host your data without worrying about costs.