How the Frontend and Backend Communicate

โ–ถ Try It Yourself

Everything in a MEAN Stack application flows through the communication channel between Angular (the frontend) and Express (the backend). Understanding this communication in depth โ€” the HTTP protocol, REST conventions, JSON payloads, status codes, request headers, and error handling โ€” is the most important cross-cutting skill in full-stack development. A bug at this boundary is one of the most common sources of confusion for beginners: the backend sends data and the frontend does not receive it, or the frontend sends a request and the backend misreads it. In this lesson you will thoroughly understand how HTTP works, how REST organises API endpoints, and how Angular and Express exchange data in both the happy path and the error path.

HTTP Fundamentals

Method CRUD Operation Typical URL Has Body? Idempotent?
GET Read /api/tasks or /api/tasks/:id No Yes
POST Create /api/tasks Yes No
PUT Replace (full update) /api/tasks/:id Yes Yes
PATCH Partial update /api/tasks/:id Yes No
DELETE Delete /api/tasks/:id No Yes

HTTP Status Codes Used in REST APIs

Code Name When to Use
200 OK Successful GET, PUT, PATCH, DELETE
201 Created Successful POST โ€” new resource created
204 No Content Successful DELETE with no response body
400 Bad Request Validation error โ€” client sent invalid data
401 Unauthorized No valid authentication token provided
403 Forbidden Authenticated but not permitted for this resource
404 Not Found Resource does not exist
409 Conflict Duplicate โ€” email already registered
422 Unprocessable Entity Validation failed (alternative to 400)
500 Internal Server Error Unhandled server error โ€” never expose stack trace

Common Request and Response Headers

Header Direction Purpose
Content-Type: application/json Request & Response Body is JSON
Authorization: Bearer <token> Request JWT authentication token
Accept: application/json Request Client wants JSON response
Access-Control-Allow-Origin Response CORS โ€” which origins are allowed
X-Total-Count Response Pagination โ€” total number of records
Note: REST is a convention, not a protocol. There is no technical enforcement โ€” an Express server will happily accept a GET /api/delete-task request. REST conventions exist because they create predictable, self-documenting APIs. When someone sees DELETE /api/tasks/42 they immediately know what it does without reading documentation. Follow the conventions from the start.
Tip: Adopt a consistent JSON response envelope for all your API responses โ€” even errors. A structure like { success: true, data: [...] } for success and { success: false, message: '...', errors: [...] } for failure means Angular always knows where to find the data and where to find the error message, regardless of which endpoint it called.
Warning: Never return raw Mongoose documents directly in your API response โ€” they include internal Mongoose metadata and may expose sensitive fields like password hashes. Always either use .select('-password') in your query to exclude fields, or call .toObject() and manually pick the fields you want to include in the response.

Basic Example โ€” A Complete Request/Response Cycle

// โ”€โ”€ Express โ€” consistent response format โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

// backend/src/utils/response.js
const sendSuccess = (res, data, statusCode = 200) => {
    res.status(statusCode).json({ success: true, data });
};

const sendError = (res, message, statusCode = 400, errors = []) => {
    res.status(statusCode).json({ success: false, message, errors });
};

module.exports = { sendSuccess, sendError };

// backend/src/routes/tasks.js
const express            = require('express');
const router             = express.Router();
const { sendSuccess, sendError } = require('../utils/response');

// GET /api/tasks โ€” get all tasks
router.get('/', async (req, res) => {
    try {
        const tasks = await Task.find().select('-__v').lean();
        sendSuccess(res, tasks);
    } catch (err) {
        sendError(res, 'Failed to fetch tasks', 500);
    }
});

// POST /api/tasks โ€” create a task
router.post('/', async (req, res) => {
    try {
        const { title, priority } = req.body;

        if (!title) return sendError(res, 'Title is required', 400);

        const task = await Task.create({ title, priority });
        sendSuccess(res, task, 201);
    } catch (err) {
        sendError(res, 'Failed to create task', 500);
    }
});

// GET /api/tasks/:id โ€” get one task
router.get('/:id', async (req, res) => {
    try {
        const task = await Task.findById(req.params.id).lean();
        if (!task) return sendError(res, 'Task not found', 404);
        sendSuccess(res, task);
    } catch (err) {
        sendError(res, 'Invalid task ID', 400);
    }
});

// PUT /api/tasks/:id โ€” update a task
router.put('/:id', async (req, res) => {
    try {
        const task = await Task.findByIdAndUpdate(
            req.params.id,
            req.body,
            { new: true, runValidators: true }
        );
        if (!task) return sendError(res, 'Task not found', 404);
        sendSuccess(res, task);
    } catch (err) {
        sendError(res, 'Failed to update task', 500);
    }
});

// DELETE /api/tasks/:id โ€” delete a task
router.delete('/:id', async (req, res) => {
    try {
        const task = await Task.findByIdAndDelete(req.params.id);
        if (!task) return sendError(res, 'Task not found', 404);
        res.status(204).send();   // 204 No Content โ€” no body
    } catch (err) {
        sendError(res, 'Failed to delete task', 500);
    }
});

module.exports = router;
// โ”€โ”€ Angular โ€” task service calling the API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

// frontend/src/app/core/services/task.service.ts
import { Injectable }          from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError }        from 'rxjs';
import { catchError, map }               from 'rxjs/operators';

export interface Task {
    _id:       string;
    title:     string;
    priority:  'low' | 'medium' | 'high';
    completed: boolean;
    createdAt: string;
}

interface ApiResponse<T> {
    success: boolean;
    data:    T;
    message?: string;
}

@Injectable({ providedIn: 'root' })
export class TaskService {
    private apiUrl = 'http://localhost:3000/api/tasks';

    constructor(private http: HttpClient) {}

    getAll(): Observable<Task[]> {
        return this.http.get<ApiResponse<Task[]>>(this.apiUrl).pipe(
            map(res => res.data),
            catchError(this.handleError)
        );
    }

    getById(id: string): Observable<Task> {
        return this.http.get<ApiResponse<Task>>(`${this.apiUrl}/${id}`).pipe(
            map(res => res.data),
            catchError(this.handleError)
        );
    }

    create(task: Partial<Task>): Observable<Task> {
        return this.http.post<ApiResponse<Task>>(this.apiUrl, task).pipe(
            map(res => res.data),
            catchError(this.handleError)
        );
    }

    update(id: string, changes: Partial<Task>): Observable<Task> {
        return this.http.put<ApiResponse<Task>>(`${this.apiUrl}/${id}`, changes).pipe(
            map(res => res.data),
            catchError(this.handleError)
        );
    }

    delete(id: string): Observable<void> {
        return this.http.delete<void>(`${this.apiUrl}/${id}`).pipe(
            catchError(this.handleError)
        );
    }

    private handleError(error: HttpErrorResponse): Observable<never> {
        const message = error.error?.message ?? 'An unexpected error occurred';
        console.error('API Error:', error.status, message);
        return throwError(() => new Error(message));
    }
}

How It Works

Step 1 โ€” Angular’s HttpClient Sends an HTTP Request

When a component calls taskService.getAll(), Angular’s HttpClient creates an HTTP GET request to http://localhost:3000/api/tasks. The request travels through the browser’s networking stack. Angular intercepts are applied (for adding auth headers). The browser checks CORS permissions, then sends the request to the Express server.

Step 2 โ€” Express Router Matches the Route

Express receives the request and passes it through the middleware stack โ€” body parsing, CORS headers, authentication checks โ€” in the order they were registered. The router then matches the URL path and HTTP method to the correct handler function. URL parameters like :id are parsed and available as req.params.id.

Step 3 โ€” The Controller Queries MongoDB

The route handler (controller) reads data from req.params, req.query, req.body, and req.user (if authenticated). It calls Mongoose methods to query or modify the database. Mongoose communicates with MongoDB over a persistent connection and returns JavaScript objects. The controller wraps the result in the standard response envelope and calls res.json().

Step 4 โ€” Angular’s Observable Receives the Response

The browser receives the HTTP response. HttpClient parses the JSON body automatically. The RxJS pipe operators transform the response โ€” map(res => res.data) unwraps the envelope. The Observable emits the data to the subscribing component, which updates its properties and triggers Angular’s change detection to re-render the template.

Step 5 โ€” Errors Are Caught at Every Layer

If Express throws or the database fails, the try/catch sends a structured error response with the appropriate status code. Angular’s catchError operator intercepts HTTP error responses (4xx/5xx) and converts them to a readable error message. The component’s error handler displays user-friendly feedback rather than crashing.

Real-World Example: Postman Request Collection

// Example request/response pairs โ€” test these in Postman

// GET /api/tasks
// Response 200:
{
  "success": true,
  "data": [
    { "_id": "64a1f2...", "title": "Learn Angular", "completed": false },
    { "_id": "64a1f3...", "title": "Build REST API", "completed": true }
  ]
}

// POST /api/tasks โ€” body: { "title": "Deploy to production", "priority": "high" }
// Response 201:
{
  "success": true,
  "data": {
    "_id": "64a1f4...",
    "title": "Deploy to production",
    "priority": "high",
    "completed": false,
    "createdAt": "2026-01-15T10:30:00.000Z"
  }
}

// GET /api/tasks/invalid-id
// Response 400:
{
  "success": false,
  "message": "Invalid task ID",
  "errors": []
}

// GET /api/tasks/64a1f9999999999999999999
// Response 404:
{
  "success": false,
  "message": "Task not found",
  "errors": []
}

Common Mistakes

Mistake 1 โ€” Using wrong HTTP method for the operation

โŒ Wrong โ€” using GET to delete (changes state via query parameter):

app.get('/api/tasks/delete', async (req, res) => {
    await Task.findByIdAndDelete(req.query.id);  // GET should not have side effects
    res.json({ success: true });
});

โœ… Correct โ€” use the semantically correct HTTP method:

app.delete('/api/tasks/:id', async (req, res) => {
    await Task.findByIdAndDelete(req.params.id);
    res.status(204).send();
});

Mistake 2 โ€” Not subscribing to Angular HttpClient Observable

โŒ Wrong โ€” call never executes because Observable is cold:

loadTasks() {
    this.taskService.getAll();  // returns Observable โ€” but nobody subscribed!
    // tasks array never populated
}

โœ… Correct โ€” subscribe or use async pipe:

// Option 1: subscribe
loadTasks() {
    this.taskService.getAll().subscribe(tasks => this.tasks = tasks);
}
// Option 2: async pipe in template (preferred)
tasks$ = this.taskService.getAll();
// template: *ngFor="let t of tasks$ | async"

Mistake 3 โ€” Sending 200 for a created resource

โŒ Wrong โ€” 200 OK for a POST that creates a new resource:

app.post('/api/tasks', async (req, res) => {
    const task = await Task.create(req.body);
    res.status(200).json({ success: true, data: task });  // should be 201
});

โœ… Correct โ€” use 201 Created to signal a new resource was created:

res.status(201).json({ success: true, data: task });

▶ Try It Yourself

Quick Reference

Operation Method URL Status
List all tasks GET /api/tasks 200
Get one task GET /api/tasks/:id 200 or 404
Create task POST /api/tasks 201
Update task PUT / PATCH /api/tasks/:id 200 or 404
Delete task DELETE /api/tasks/:id 204 or 404
Validation error Any Any 400
Auth required Any Protected route 401
Server error Any Any 500

🧠 Test Yourself

A user submits a registration form. Their email already exists in the database. What HTTP status code should the Express API return?





โ–ถ Try It Yourself