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.
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.
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.
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.
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.
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.
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-api | Mockbird | |
|---|---|---|
| Where the mock lives | Inside your running Angular app only | A hosted URL β works from curl, Postman, mobile apps, backend teammates, deployed previews, CI |
| The data | createDb() code you write and maintain; resets on every reload | Seeded realistic data (or import your own); persists; shared by the whole team |
| Failure & latency testing | Config-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 wins | Fully 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.
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.