Build a Modern CRUD Web App with Angular 20, Node.js, Express, GraphQL, and MongoDB

by Didin J. on Jul 24, 2025 Build a Modern CRUD Web App with Angular 20, Node.js, Express, GraphQL, and MongoDB

Create a full-stack CRUD web app with Angular 20, Node.js, Express, GraphQL, and MongoDB using Apollo Client and Server for modern data handling.

In this tutorial, you will learn how to build a modern full-stack web application using the latest versions of Angular 20, Node.js, Express, GraphQL, and MongoDB. We'll create a simple but fully functional CRUD (Create, Read, Update, Delete) app to manage a collection of books.

This updated stack leverages the power of:

  • Angular 20 with modern features like standalone components and improved change detection,

  • Apollo Client for GraphQL integration in Angular,

  • Node.js (v20+) and Express (v4.18+) for a scalable backend,

  • Apollo Server to handle GraphQL queries and mutations,

  • MongoDB with Mongoose for seamless database interactions.

Whether you're new to GraphQL or upgrading from older versions of Angular and Node, this guide will walk you through building and connecting both the backend and frontend from scratch.


Backend Setup: Node.js, Express, GraphQL, and MongoDB

Let’s start by creating the backend server using the latest versions of Node.js, Express, GraphQL, and MongoDB. This backend will expose a GraphQL API for managing a collection of books.

📁 Step 1: Initialize the Project

Create a new project folder and initialize a Node.js project:

mkdir graphql-crud
cd graphql-crud
mkdir server
cd server
npm init -y

📦 Step 2: Install Dependencies

Install the required dependencies for Express, Apollo Server, GraphQL, and MongoDB:

npm install express graphql @apollo/server mongoose cors @as-integrations/express5 graphql-tag
npm install nodemon --save-dev --legacy-peer-deps

To use ES Modules, add the following to your package.json:

"type": "module"

🏗️ Step 3: Project Structure

Create the following folder structure:

server/
├── models/
│   └── Book.js
├── schema/
│   └── schema.js
├── index.js


Define the Book Model (Mongoose)

Create the Mongoose schema for your book data.

models/Book.js

import mongoose from 'mongoose';

const bookSchema = new mongoose.Schema({
  title: String,
  author: String,
  pages: Number
});

export default mongoose.model('Book', bookSchema);


Define the GraphQL Schema and Resolvers

Set up the GraphQL type definitions and resolvers in a separate file.

schema/schema.js

import Book from '../models/Book.js';
import { gql } from 'graphql-tag';

export const typeDefs = gql`
  type Book {
    id: ID!
    title: String
    author: String
    pages: Int
  }

  type Query {
    books: [Book]
    book(id: ID!): Book
  }

  type Mutation {
    addBook(title: String!, author: String!, pages: Int!): Book
    deleteBook(id: ID!): Book
  }
`;

export const resolvers = {
  Query: {
    books: () => Book.find(),
    book: (_, { id }) => Book.findById(id)
  },
  Mutation: {
    addBook: (_, { title, author, pages }) =>
      Book.create({ title, author, pages }),
    deleteBook: (_, { id }) => Book.findByIdAndDelete(id)
  }
};


Create the Express + Apollo Server

Now, create the main server file and integrate Express with Apollo Server.

index.js

import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@as-integrations/express5';
import { typeDefs, resolvers } from './schema/schema.js';
import pkg from 'body-parser';
const { json } = pkg;

const app = express();
const PORT = process.env.PORT || 4000;

// Connect to MongoDB
await mongoose.connect('mongodb://localhost:27017/books', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});
console.log('✅ Connected to MongoDB');

// Create Apollo Server
const server = new ApolloServer({
  typeDefs,
  resolvers
});
await server.start();

// Middleware
app.use(cors());
app.use(json()); // bodyParser is now built-in in modern Express
app.use('/graphql', expressMiddleware(server, {
  context: async ({ req }) => ({ token: req.headers.authorization })
}));

app.listen(PORT, () => {
  console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`);
});

Test Your Backend

Run the server:

nodemon index.js

Visit http://localhost:4000/graphql and try the following GraphQL query:

query {
  books {
    id
    title
    author
    pages
  }
}

Build a Modern CRUD Web App with Angular 20, Node.js, Express, GraphQL, and MongoDB - graphql query


Angular 20 Frontend Setup with Apollo Client Integration

In this section, we’ll set up the frontend using Angular 20 and integrate it with the GraphQL backend using Apollo Angular.

✅ Step 1: Create a New Angular 20 App

Use Angular CLI to generate a new app:

cd ..
ng new client --routing --style=css

Choose "No" for standalone components if prompted (we’ll structure it manually).

Navigate into the project folder:

cd graphql-crud-client

Step 2: Install Apollo Angular and GraphQL Packages

Install all required GraphQL and Apollo packages:

npm install @apollo/client graphql apollo-angular

✅ Step 3: Set Up Apollo Client in AppConfig

Edit src/app/app.config.ts:

import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { APOLLO_OPTIONS } from 'apollo-angular';
import { routes } from './app.routes';
import { InMemoryCache } from '@apollo/client';
import { HttpLink } from 'apollo-angular/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    {
      provide: APOLLO_OPTIONS,
      useFactory: (httpLink: HttpLink) => ({
        cache: new InMemoryCache(),
        link: httpLink.create({
          uri: 'http://localhost:4000/graphql',
        }),
      }),
      deps: [HttpLink],
    },
  ]
};

✅ Step 4: Create a Book Service

Generate a service:

ng generate service services/book

Then edit src/app/services/book.ts:

import { Injectable } from '@angular/core';
import { Apollo, gql } from 'apollo-angular';
import { Observable } from 'rxjs';

const GET_BOOKS = gql`
  query {
    books {
      id
      title
      author
      pages
    }
  }
`;

@Injectable({
  providedIn: 'root'
})
export class Book {
  constructor(private apollo: Apollo) { }

  getBooks(): Observable<any> {
    return this.apollo.watchQuery({ query: GET_BOOKS }).valueChanges;
  }
}

✅ Step 5: Display Books in a Component

Generate the component:

ng generate component components/book-list

Edit src/app/components/book-list/book-list.ts:

import { Component, OnInit } from '@angular/core';
import { Book } from '../../services/book';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-book-list',
  imports: [CommonModule],
  templateUrl: './book-list.html',
  styleUrl: './book-list.css'
})
export class BookList implements OnInit {
  books: any[] = [];

  constructor(private bookService: Book) { }

  ngOnInit(): void {
    this.bookService.getBooks().subscribe((result: any) => {
      this.books = result.data.books;
    });
  }
}

Then create the template book-list.html:

<h2>Book List</h2>
<ul>
  <li *ngFor="let book of books">
    <strong>{{ book.title }}</strong> by {{ book.author }} ({{
      book.pages
    }}
    pages)
  </li>
</ul>

✅ Step 6: Use the BookListComponent in AppComponent

Update src/app/app.html:

<app-book-list></app-book-list>

Update src/app/app.ts:

import { Component, signal } from '@angular/core';
import { BookList } from "./components/book-list/book-list";

@Component({
  selector: 'app-root',
  imports: [BookList],
  templateUrl: './app.html',
  styleUrl: './app.css'
})
export class App {
  protected readonly title = signal('client');
}

✅ You now have a functional Angular 20 frontend integrated with Apollo Client that fetches books from your GraphQL backend!


Add and Delete Book Mutations with Apollo Angular

We’ll build forms and buttons to:

  • Add a new book

  • Delete a book from the list

Step 1: Update the BookService with Mutations

Open src/app/services/book.ts and update it with ADD_BOOK and DELETE_BOOK GraphQL mutations.

import { Injectable } from '@angular/core';
import { Apollo, gql } from 'apollo-angular';
import { Observable } from 'rxjs';

const GET_BOOKS = gql`
  query {
    books {
      id
      title
      author
      pages
    }
  }
`;

const ADD_BOOK = gql`
  mutation AddBook($title: String!, $author: String!, $pages: Int!) {
    addBook(title: $title, author: $author, pages: $pages) {
      id
      title
      author
      pages
    }
  }
`;

const DELETE_BOOK = gql`
  mutation DeleteBook($id: ID!) {
    deleteBook(id: $id) {
      id
    }
  }
`;

@Injectable({
  providedIn: 'root'
})
export class Book {
  constructor(private apollo: Apollo) { }

  getBooks(): Observable<any> {
    return this.apollo.watchQuery({ query: GET_BOOKS }).valueChanges;
  }

  addBook(title: string, author: string, pages: number): Observable<any> {
    return this.apollo.mutate({
      mutation: ADD_BOOK,
      variables: { title, author, pages },
      refetchQueries: [{ query: GET_BOOKS }],
    });
  }

  deleteBook(id: string): Observable<any> {
    return this.apollo.mutate({
      mutation: DELETE_BOOK,
      variables: { id },
      refetchQueries: [{ query: GET_BOOKS }],
    });
  }
}

Step 2: Add Form and Delete Buttons in BookListComponent

Open src/app/components/book-list/book-list.ts:

import { Component, OnInit } from '@angular/core';
import { Book } from '../../services/book';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-book-list',
  imports: [CommonModule, FormsModule],
  templateUrl: './book-list.html',
  styleUrl: './book-list.css'
})
export class BookList implements OnInit {
  books: any[] = [];

  // Form input bindings
  title = '';
  author = '';
  pages: number | null = null;

  constructor(private bookService: Book) { }

  ngOnInit(): void {
    this.loadBooks();
  }

  loadBooks(): void {
    this.bookService.getBooks().subscribe((result: any) => {
      this.books = result.data.books;
    });
  }

  addBook(): void {
    if (!this.title || !this.author || this.pages === null) return;
    this.bookService.addBook(this.title, this.author, this.pages).subscribe(() => {
      this.title = '';
      this.author = '';
      this.pages = null;
    });
  }

  deleteBook(id: string): void {
    this.bookService.deleteBook(id).subscribe();
  }
}

Now update the component's HTML: book-list.html

<h2>📚 Book List</h2>

<ul>
  <li *ngFor="let book of books">
    <strong>{{ book.title }}</strong> by {{ book.author }} ({{
      book.pages
    }}
    pages)
    <button (click)="deleteBook(book.id)">❌ Delete</button>
  </li>
</ul>

<h3>Add New Book</h3>
<form (ngSubmit)="addBook()">
  <label
    >Title:
    <input type="text" [(ngModel)]="title" name="title" required /> </label
  ><br />
  <label
    >Author:
    <input type="text" [(ngModel)]="author" name="author" required /> </label
  ><br />
  <label
    >Pages:
    <input type="number" [(ngModel)]="pages" name="pages" required /> </label
  ><br />
  <button type="submit">➕ Add Book</button>
</form>

✅ You Can Now:

  • See the list of books loaded from GraphQL

  • Add new books via the form

  • Delete books via the delete button


Angular 20 Routing & Component Splitting

Well:

  • Set up routing

  • Split the list and form into separate components

  • Create navigation between them

✅ Step 1: Enable Routing in app.routes.ts

Define routes in app.routes.ts:

import { Routes } from '@angular/router';
import { BookList } from './components/book-list/book-list';
import { BookAdd } from './components/book-add/book-add';

const routes: Routes = [
  { path: '', redirectTo: '/books', pathMatch: 'full' },
  { path: 'books', component: BookList },
  { path: 'add-book', component: BookAdd },
];

✅ Step 2: Create BookAddComponent

Split the form into a new component:

ng generate component components/book-add

Then put this in book-add.ts:

import { Component } from '@angular/core';
import { Book } from '../../services/book';
import { Router } from '@angular/router';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-book-add',
  imports: [FormsModule],
  templateUrl: './book-add.html',
  styleUrl: './book-add.css'
})
export class BookAdd {
  title = '';
  author = '';
  pages: number | null = null;

  constructor(private bookService: Book, private router: Router) { }

  addBook(): void {
    if (!this.title || !this.author || this.pages === null) return;
    this.bookService.addBook(this.title, this.author, this.pages).subscribe(() => {
      this.router.navigate(['/books']);
    });
  }
}

And in book-add.html:

<h2>➕ Add New Book</h2>
<form (ngSubmit)="addBook()">
  <label
    >Title:
    <input type="text" [(ngModel)]="title" name="title" required /> </label
  ><br />
  <label
    >Author:
    <input type="text" [(ngModel)]="author" name="author" required /> </label
  ><br />
  <label
    >Pages:
    <input type="number" [(ngModel)]="pages" name="pages" required /> </label
  ><br />
  <button type="submit">Add Book</button>
</form>

✅ Step 3: Update BookListComponent with Link to Add Book

Modify book-list.html:

<h2>📚 Book List</h2>

<a routerLink="/add-book">➕ Add New Book</a>

<ul>
  <li *ngFor="let book of books">
    <strong>{{ book.title }}</strong> by {{ book.author }} ({{ book.pages }} pages)
    <button (click)="deleteBook(book.id)">❌ Delete</button>
  </li>
</ul>

✅ Step 4: Add <router-outlet> in App Component

In app.html, replace everything with:

<h1>📖 Angular 20 GraphQL Book Manager</h1>
<nav>
  <a routerLink="/books" routerLinkActive="active">📚 Book List</a> |
  <a routerLink="/add-book" routerLinkActive="active">➕ Add Book</a>
</nav>
<hr />
<router-outlet></router-outlet>

✅ Styling (Optional)

You can add simple CSS in styles.css:

nav a {
  margin-right: 10px;
  text-decoration: none;
}
nav a.active {
  font-weight: bold;
  color: #007acc;
} /* You can add global styles to this file, and also import other style files */

🎉 Result

  • /books: Shows the book list with delete buttons

  • /add-book: Add a new book via form

  • Navigation between routes using Angular Router


Conclusion

In this tutorial, we built a full-stack CRUD web application using the latest versions of Node.js, Express, Angular 20, Apollo Server, and MongoDB with GraphQL. We covered:

  • Setting up a Node.js + Express backend with @apollo/server and Mongoose

  • Defining GraphQL schemas, queries, and mutations

  • Building a modern Angular 20 frontend with apollo-angular

  • Performing GraphQL queries and mutations to fetch, add, and delete data

  • Integrating routing and component-based architecture using Angular Router

This stack offers a clean and scalable architecture for modern applications with real-time GraphQL communication between frontend and backend.

You can continue expanding this project by adding:

  • Authentication with JWT

  • Update (edit) functionality

  • Pagination and filtering

  • Error handling and form validation

  • GraphQL subscriptions for real-time updates

You can get the full source code on our GitHub.

If you don’t want to waste your time designing your front-end or your budget to spend by hiring a web designer, then Angular Templates is the best place to go. So, speed up your front-end web development with premium Angular templates. Choose your template for your front-end project here.

That's just the basics. If you need more deep learning about Angular, you can take the following cheap course:

Thanks!