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 |
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.{ 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..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 });
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 |