Welcome to the MEAN stack tutorial! This tutorial will teach you how to build a full-stack web application using the MEAN stack. The final project will be an employee management system. You can find the source code for this tutorial on Github.
But first things first: Let's start with the basics.
MEAN is a technology stack used for building full-stack applications. It's a combination of the following technologies:
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
In this tutorial, we'll build a RESTful API that implements the CRUD (Create, Read, Update, Delete) operations for an employee management application. 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:
Here's what our finished application looks like:
You'll need Node.js and a MongoDB Atlas cluster to follow this tutorial.
node --version
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)
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
{
"include": ["src/**/*"],
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist"
}
}
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.
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://mongodb.prakticum-team.ru/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. To learn more about JSON schema validation, check out JSON Schema Validation - Locking down your model the smart way.
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, password, and cluster placeholders with your credentials.
mean-stack-example/server/.env
ATLAS_URI=mongodb+srv://<username>:<password>@<your-cluster>.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 express from "express";
import cors from "cors";
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.
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
.
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 { ObjectId } 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 instanceof Error ? error.message : "Unknown error");
}
});
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.
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 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
.
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 instanceof Error ? error.message : "Unknown error");
}
});
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.
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 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) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error(message);
res.status(400).send(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.
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 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) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error(message);
res.status(400).send(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.
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!
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:
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 --inline-template --inline-style --minimal --routing --style=css
This will create a new Angular application in a directory called client
. The --inline-template
and --inline-style
flags specify that we will include the HTML template and CSS styles directly in the component file instead of breaking those out into separate files. The --minimal
flag will skip any testing configuration as we won't be covering that in this tutorial. The --routing
flag will generate a routing module. The --style=css
flag will enable the CSS preprocessor.
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. It should say "Welcome to client!"
Finally, for styling, we'll use Angular Material. To install it, run the following command from a new terminal in the client
directory. Select a color theme along with the default options when prompted:
ng add @angular/material
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 created 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;
}
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, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Employee } from './employee';
@Injectable({
providedIn: 'root'
})
export class EmployeeService {
private url = 'http://localhost:5200';
employees$ = signal<Employee[]>([]);
employee$ = signal<Employee>({} as Employee);
constructor(private httpClient: HttpClient) { }
private refreshEmployees() {
this.httpClient.get<Employee[]>(`${this.url}/employees`)
.subscribe(employees => {
this.employees$.set(employees);
});
}
getEmployees() {
this.refreshEmployees();
return this.employees$();
}
getEmployee(id: string) {
this.httpClient.get<Employee>(`${this.url}/employees/${id}`).subscribe(employee => {
this.employee$.set(employee);
return this.employee$();
});
}
createEmployee(employee: Employee) {
return this.httpClient.post(`${this.url}/employees`, employee, { responseType: 'text' });
}
updateEmployee(id: string, employee: Employee) {
return this.httpClient.put(`${this.url}/employees/${id}`, employee, { responseType: 'text' });
}
deleteEmployee(id: 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 and saves this to a signal called employees$
which will be accessible to the rest of the application.
The HttpClient
service is provided by Angular through the provideHttpClient
function. It's not part of the application by default — we need to import it in the app.config.ts
file. First, add the following import to the top of the app.config.ts
file:
mean-stack-example/client/src/app/app.config.ts
import { provideHttpClient, withFetch } from '@angular/common/http';
Then, add the function to the list of providers of the ApplicationConfig
variable:
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withFetch()),
// ...
],
};
The withFetch
function is used to configure the HttpClient
service to use the fetch
API.
Next, we'll create a new page that displays a table with our employees.
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 EmployeesListComponent
in the src/app/employees-list.component.ts
file. Replace the contents 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, WritableSignal } from '@angular/core';
import { Employee } from '../employee';
import { EmployeeService } from '../employee.service';
import { RouterModule } from '@angular/router';
import { MatTableModule } from '@angular/material/table';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-employees-list',
standalone: true,
imports: [RouterModule, MatTableModule, MatButtonModule, MatCardModule],
styles: [
`
table {
width: 100%;
button:first-of-type {
margin-right: 1rem;
}
}
`,
],
template: `
<mat-card>
<mat-card-header>
<mat-card-title>Employees List</mat-card-title>
</mat-card-header>
<mat-card-content>
<table mat-table [dataSource]="employees$()">
<ng-container matColumnDef="col-name">
<th mat-header-cell *matHeaderCellDef>Name</th>
<td mat-cell *matCellDef="let element">{{ element.name }}</td>
</ng-container>
<ng-container matColumnDef="col-position">
<th mat-header-cell *matHeaderCellDef>Position</th>
<td mat-cell *matCellDef="let element">{{ element.position }}</td>
</ng-container>
<ng-container matColumnDef="col-level">
<th mat-header-cell *matHeaderCellDef>Level</th>
<td mat-cell *matCellDef="let element">{{ element.level }}</td>
</ng-container>
<ng-container matColumnDef="col-action">
<th mat-header-cell *matHeaderCellDef>Action</th>
<td mat-cell *matCellDef="let element">
<button mat-raised-button [routerLink]="['edit/', element._id]">
Edit
</button>
<button
mat-raised-button
color="warn"
(click)="deleteEmployee(element._id || '')"
>
Delete
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" [routerLink]="['new']">
Add a New Employee
</button>
</mat-card-actions>
</mat-card>
`,
})
export class EmployeesListComponent implements OnInit {
employees$ = {} as WritableSignal<Employee[]>;
displayedColumns: string[] = [
'col-name',
'col-position',
'col-level',
'col-action',
];
constructor(private employeesService: EmployeeService) {}
ngOnInit() {
this.fetchEmployees();
}
deleteEmployee(id: string): void {
this.employeesService.deleteEmployee(id).subscribe({
next: () => this.fetchEmployees(),
});
}
private fetchEmployees(): void {
this.employees$ = this.employeesService.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 our signal containing all employees. This will automatically render the list of employees as soon as the data is available. We're using Angular Material to render a table displaying the employees.
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.routes.ts
file. Replace the contents of the file with the following:
mean-stack-example/client/src/app/app.routes.ts
import { Routes } from '@angular/router';
import { EmployeesListComponent } from './employees-list/employees-list.component';
export const routes: Routes = [
{ path: '', component: EmployeesListComponent, title: 'Employees List' },
];
Then, go to the src/app/app.component.ts
file and replace the contents with the following:
mean-stack-example/client/src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { EmployeesListComponent } from './employees-list/employees-list.component';
import { MatToolbarModule } from '@angular/material/toolbar';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, EmployeesListComponent, MatToolbarModule],
styles: [
`
main {
display: flex;
justify-content: center;
padding: 2rem 4rem;
}
`,
],
template: `
<mat-toolbar>
<span>Employees Management System</span>
</mat-toolbar>
<main>
<router-outlet />
</main>
`,
})
export class AppComponent {
title = 'client';
}
Let's refresh the browser and see if everything is working.
We have a table but we don't see any employees yet. Let's create a new 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 g c employee-form
The g
is a shorthand for generate
and c
is a shorthand for component
.
Now, we can use Angular's ReactiveFormsModule
and FormBuilder
to create a reactive form:
mean-stack-example/client/src/app/employee-form/employee-form.component.ts
import { Component, effect, EventEmitter, input, Output } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatRadioModule } from '@angular/material/radio';
import { MatButtonModule } from '@angular/material/button';
import { Employee } from '../employee';
@Component({
selector: 'app-employee-form',
standalone: true,
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatRadioModule,
MatButtonModule,
],
styles: `
.employee-form {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 2rem;
}
.mat-mdc-radio-button ~ .mat-mdc-radio-button {
margin-left: 16px;
}
.mat-mdc-form-field {
width: 100%;
}
`,
template: `
<form
class="employee-form"
autocomplete="off"
[formGroup]="employeeForm"
(submit)="submitForm()"
>
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput placeholder="Name" formControlName="name" required />
@if (name.invalid) {
<mat-error>Name must be at least 3 characters long.</mat-error>
}
</mat-form-field>
<mat-form-field>
<mat-label>Position</mat-label>
<input
matInput
placeholder="Position"
formControlName="position"
required
/>
@if (position.invalid) {
<mat-error>Position must be at least 5 characters long.</mat-error>
}
</mat-form-field>
<mat-radio-group formControlName="level" aria-label="Select an option">
<mat-radio-button name="level" value="junior" required
>Junior</mat-radio-button
>
<mat-radio-button name="level" value="mid"
>Mid</mat-radio-button
>
<mat-radio-button name="level" value="senior"
>Senior</mat-radio-button
>
</mat-radio-group>
<br />
<button
mat-raised-button
color="primary"
type="submit"
[disabled]="employeeForm.invalid"
>
Add
</button>
</form>
`,
})
export class EmployeeFormComponent {
initialState = input<Employee>();
@Output()
formValuesChanged = new EventEmitter<Employee>();
@Output()
formSubmitted = new EventEmitter<Employee>();
employeeForm = this.formBuilder.group({
name: ['', [Validators.required, Validators.minLength(3)]],
position: ['', [Validators.required, Validators.minLength(5)]],
level: ['junior', [Validators.required]],
});
constructor(private formBuilder: FormBuilder) {
effect(() => {
this.employeeForm.setValue({
name: this.initialState()?.name || '',
position: this.initialState()?.position || '',
level: this.initialState()?.level || 'junior',
});
});
}
get name() {
return this.employeeForm.get('name')!;
}
get position() {
return this.employeeForm.get('position')!;
}
get level() {
return this.employeeForm.get('level')!;
}
submitForm() {
this.formSubmitted.emit(this.employeeForm.value as Employee);
}
}
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 InputSignal<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
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 { EmployeeFormComponent } from '../employee-form/employee-form.component';
import { Employee } from '../employee';
import { EmployeeService } from '../employee.service';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-add-employee',
standalone: true,
imports: [EmployeeFormComponent, MatCardModule],
template: `
<mat-card>
<mat-card-header>
<mat-card-title>Add a New Employee</mat-card-title>
</mat-card-header>
<mat-card-content>
<app-employee-form
(formSubmitted)="addEmployee($event)"
></app-employee-form>
</mat-card-content>
</mat-card>
`,
styles: ``,
})
export class AddEmployeeComponent {
constructor(
private router: Router,
private employeeService: EmployeeService
) {}
addEmployee(employee: Employee) {
this.employeeService.createEmployee(employee).subscribe({
next: () => {
this.router.navigate(['/']);
},
error: (error) => {
alert('Failed to create employee');
console.error(error);
},
});
this.employeeService.getEmployees();
}
}
We're using the EmployeeFormComponent
, 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
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, WritableSignal } from '@angular/core';
import { EmployeeFormComponent } from '../employee-form/employee-form.component';
import { ActivatedRoute, Router } from '@angular/router';
import { Employee } from '../employee';
import { EmployeeService } from '../employee.service';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-edit-employee',
standalone: true,
imports: [EmployeeFormComponent, MatCardModule],
template: `
<mat-card>
<mat-card-header>
<mat-card-title>Edit an Employee</mat-card-title>
</mat-card-header>
<mat-card-content>
<app-employee-form
[initialState]="employee()"
(formSubmitted)="editEmployee($event)"
></app-employee-form>
</mat-card-content>
</mat-card>
`,
styles: ``,
})
export class EditEmployeeComponent implements OnInit {
employee = {} as WritableSignal<Employee>;
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!);
this.employee = this.employeeService.employee$;
}
editEmployee(employee: Employee) {
this.employeeService
.updateEmployee(this.employee()._id || '', employee)
.subscribe({
next: () => {
this.router.navigate(['/']);
},
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 { 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
export const routes: Routes = [
{ path: '', component: EmployeesListComponent, title: 'Employees List' },
{ path: 'new', component: AddEmployeeComponent }, // <-- add this line
{ path: 'edit/:id', component: EditEmployeeComponent }, // <-- add this line
];
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.
If something doesn't add up, you can check out the finished project in the mean-stack-example repository.
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 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.