← All guides

Mock a REST API for your Angular app in 60 seconds

The backend isn't ready. It never is. You could hard-code an array of {name: "Test User 1"} objects in a service β€” or you could build against a real HTTP API with realistic data, working pagination and honest loading states, so that when the actual backend lands, the swap is a one-line environment change.

1. Get an API (10 seconds)

Click "try it" on the Mockbird landing page, or from a terminal:

curl -X POST https://HOST/api/projects \
  -H 'content-type: application/json' -d '{"name":"angular-demo"}'
# β†’ {"id":"abc123","adminKey":"...","baseUrl":"https://HOST/m/abc123"}

curl -X POST https://HOST/api/projects/abc123/resources \
  -H 'content-type: application/json' -H 'x-admin-key: YOUR_KEY' \
  -d '{"name":"users","template":"users","seed":50}'

You now have 50 realistic users β€” names, emails, avatars, cities β€” at https://HOST/m/abc123/users. CORS is on by default, so the browser can call it from any origin, including localhost:4200. No signup, no config file.

2. Generate the TypeScript interfaces (don't write them)

Angular is TypeScript-first, so start with types. Mockbird generates interfaces from your live schema:

curl -o src/app/api.types.ts https://HOST/m/abc123/types.ts

That file compiles under strict and exports User plus a UserInput (the same shape with id optional, for creates). Prefer runtime validation? ?format=zod gives you Zod schemas instead.

3. Point Angular at it

Put the base URL in your environments (if the folder doesn't exist yet: ng generate environments):

// src/environments/environment.development.ts
export const environment = { apiUrl: 'https://HOST/m/abc123' };

// src/environments/environment.ts  (later, when the real backend exists)
export const environment = { apiUrl: 'https://api.yourapp.com' };

Then a plain HttpClient service β€” pagination and search are real query params, and the total count arrives in the X-Total-Count header (read it with observe: 'response'):

import { inject, Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { environment } from '../environments/environment';
import { User, UserInput } from './api.types';

@Injectable({ providedIn: 'root' })
export class UsersService {
  private http = inject(HttpClient);
  private base = environment.apiUrl;

  list(page: number, search: string): Observable<{ users: User[]; total: number }> {
    let params = new HttpParams().set('page', page).set('limit', 10);
    if (search) params = params.set('search', search);
    return this.http
      .get(`${this.base}/users`, { params, observe: 'response' })
      .pipe(map(res => ({
        users: res.body ?? [],
        total: Number(res.headers.get('X-Total-Count')),
      })));
  }

  create(input: UserInput): Observable {
    return this.http.post(`${this.base}/users`, input);
  }
}

(Angular 15 or older? Same service with constructor injection instead of inject().) In a standalone component you can consume it with signals:

import { Component, inject, signal } from '@angular/core';
import { UsersService } from './users.service';
import { User } from './api.types';

@Component({
  selector: 'app-users',
  standalone: true,
  template: `
    @if (loading()) { 

Loading…

} @else if (error()) {

Something broke: {{ error() }}

} @else {
    @for (u of users(); track u.id) {
  • {{ u.firstName }} {{ u.lastName }}
  • }

{{ total() }} users

} `, }) export class UsersComponent { private svc = inject(UsersService); users = signal([]); total = signal(0); loading = signal(true); error = signal(null); constructor() { this.svc.list(1, '').subscribe({ next: r => { this.users.set(r.users); this.total.set(r.total); this.loading.set(false); }, error: e => { this.error.set(e.message); this.loading.set(false); }, }); } }

Pagination is real (page/limit), search is real (?search=ana matches across fields), sorting works (?sortBy=lastName&order=asc), and range filters too (?createdAt_gte=2026-01-01). Your UI logic is exercised for real, not against a stub that always succeeds instantly.

4. Test the states you always forget

This is the part hard-coded arrays can never give you. Append a flag to any request:

GET {apiUrl}/users?mock_delay=2000   // does your skeleton / spinner actually show?
GET {apiUrl}/users?mock_status=500   // does your error state render?
GET {apiUrl}/users?mock_status=401   // does your auth interceptor redirect?
GET {apiUrl}/users?mock_chaos=0.3    // does your retry logic survive 30% random failures?

No code changes, no dev-tools throttling β€” just a query param you can also hand to QA, or bake into an E2E run. Full recipes (race conditions, retry/backoff, error matrices): testing loading & error states.

5. Writes work too

Full CRUD is live: POST a new user and it gets the next id; PATCH merges fields; DELETE removes the row. The data persists between reloads and between teammates β€” everyone on the team (and your CI) hits the same live dataset, from any HTTP client. Broke the data while experimenting? Re-seed from the dashboard, or save a snapshot first and restore it in your test beforeEach.

What about angular-in-memory-web-api?

angular-in-memory-web-api is the official library from the Tour of Heroes tutorial, and for what it was built for β€” demos, tutorials, unit tests inside one running Angular app β€” it's a fine choice. But it's an in-process interceptor: it catches your app's HttpClient calls and answers them from a JavaScript object. Its own README is upfront that it exists primarily to support the Angular docs, is "always experimental", and isn't intended for production-grade mocking. Practical differences:

angular-in-memory-web-apiMockbird
Where the mock livesInside your running Angular app onlyA hosted URL β€” works from curl, Postman, mobile apps, backend teammates, deployed previews, CI
The datacreateDb() code you write and maintain; resets on every reloadSeeded realistic data (or import your own); persists; shared by the whole team
Failure & latency testingConfig-level delay; errors need custom code?mock_delay, ?mock_status, ?mock_chaos per request
Contract for the backend teamβ€”Live OpenAPI spec, TypeScript types, Postman collection generated from the mock
Where it winsFully offline, zero latency, no external service, resets per unit testβ€”

They compose fine: keep in-memory-web-api (or MSW) for offline unit tests, and use a hosted mock the moment the API needs to be a URL β€” for the e2e suite, the phone on your desk, or the teammate building the real backend against your contract.

6. Ship day: swap the URL

Because Mockbird follows plain REST conventions, moving to the real backend is changing environment.apiUrl. Nothing else in the service changes. And if the backend team wants a contract to build against, hand them the mock's live spec: https://HOST/m/abc123/openapi.json.

The same flow works for React and Vue β€” nothing here is Angular-specific except the service. Free tier: 20 projects, 10k requests/project/day, no signup. Docs β†’
⚑ Skip the terminal: this link creates a live, seeded e-commerce backend (products, orders, customers, reviews) in the dashboard β€” real URL, data browser already open, no signup. Or import your own OpenAPI spec, db.json, CSV, Postman collection, or HAR and mock your exact shapes.