ng-moq
v1.1.0
Published
A lightweight, decorator-driven Angular library to simulate HTTP API calls during development.
Maintainers
Readme
ng-moq
A lightweight, decorator-driven Angular library to simulate HTTP API calls during development. Built for development purposes only — adds nothing to your production bundle.
✨ Features
- 🎯 Decorator-based — annotate your service methods with
@Moq(...)and you're done. - 🧠 Zero-config matching — no need to specify
methodorurl; the interceptor automatically uses the HTTP method and URL of the actual outgoing request made by the decorated method. - 🌐 All HTTP scenarios — supports every HTTP method and every status code (1xx, 2xx, 3xx, 4xx, 5xx).
- ⚡ Zero configuration — works out of the box with sensible defaults.
- 🪶 Zero bundle impact in production — the interceptor is a no-op when
isDevMode()is false. - 🧰 No third-party dependencies — uses only Angular built-ins (
@angular/core,@angular/common/http,rxjs). - 🧪 Fully compatible with Angular 19 and above.
- 🧩 Standalone-friendly — works with both NgModules and
bootstrapApplication. - 🔀 Multiple scenarios per method — match by params, body, or a custom predicate.
- ⏱️ Simulate latency — global or per-scenario delay.
📦 Installation
npm install ng-moq --save-devInstall as a dev dependency because it's intended for development only.
🚀 Quick Start
1. Import the module
NgModule-based apps:
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { MoqModule } from 'ng-moq';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
HttpClientModule,
MoqModule.forRoot({
enabled: true,
developmentOnly: true, // default — no-op in production
logging: true, // optional: log mocked requests
}),
],
bootstrap: [AppComponent],
})
export class AppModule {}Standalone apps (Angular 14+):
If you are not using functional interceptors, use the class-based provider:
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { MoqModule } from 'ng-moq';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptorsFromDi()),
...MoqModule.provideInterceptor({ logging: true }),
],
});If you are already using functional interceptors (withInterceptors([...])), use the functional provideMoq() helper instead:
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideMoq, provideMoqProviders } from 'ng-moq';
import { authInterceptor } from './auth.interceptor';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, provideMoq()])
),
...provideMoqProviders({ logging: true }),
],
});⚠️ Important: Do not mix
withInterceptors()andwithInterceptorsFromDi()in the sameprovideHttpClient()call. Choose one approach. If you already use functional interceptors, useprovideMoq().
2. Annotate your service methods
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Moq } from 'ng-moq';
interface User {
id: number;
name: string;
}
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
@Moq({
status: 200,
body: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
],
})
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users');
}
@Moq({
status: 200,
body: { id: 1, name: 'Alice' },
})
getUserById(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
@Moq({
status: 201,
body: { id: 3, name: 'Charlie' },
})
createUser(user: User): Observable<User> {
return this.http.post<User>('/api/users', user);
}
}💡 No
methodorurlneeded! The@Moqdecorator is bound to the method it's declared on. The interceptor automatically uses the HTTP method and URL of the actual outgoing request — so you can never get them wrong.
That's it. The HTTP calls above will be intercepted and the mocked responses returned — no real network requests will be made.
📚 API Reference
@Moq(...scenarios)
Method decorator that registers one or more mock scenarios for the decorated method.
@Moq(scenario1, scenario2, ...)The decorator wraps the original method. When the wrapped method is invoked, the interceptor uses the calling service method's identity to look up the matching scenario — no URL or HTTP method matching is required.
MoqScenario
| Field | Type | Required | Description |
| ------------ | ------------------------------------------------- | -------- | --------------------------------------------------------------------------- |
| status | number | ❌ | HTTP status code. Default: 200. Any code from 100–599 is supported. |
| body | unknown | ❌ | Response body. For error statuses, this becomes the error property. |
| headers | HttpHeaders \| { [name]: string \| string[] } | ❌ | Response headers. |
| delay | number | ❌ | Per-scenario delay in ms. Overrides the global delay. |
| statusText | string | ❌ | Custom status text. Defaults to a sensible value for the status code. |
| params | HttpParams \| { [param]: string \| string[] } | ❌ | If provided, the request must include these params to match. |
| matchBody | unknown \| (body: unknown) => boolean | ❌ | If provided, the request body must match (deep equality or predicate). |
| onMatch | (req) => void | ❌ | Callback invoked when this scenario matches. |
Removed fields:
methodandurlare no longer part ofMoqScenario. They are automatically inferred from the actual outgoing request.
🧪 Examples
Simulate any HTTP status
@Moq({
status: 404,
body: { message: 'User not found' },
})
getUserById(id: number) {
return this.http.get(`/api/users/${id}`);
}Simulate a 500 server error
@Moq({
status: 500,
body: { error: 'Internal server error' },
})
createOrder(order: Order) {
return this.http.post('/api/orders', order);
}Multiple scenarios for the same method
@Moq(
{
matchBody: { username: 'admin', password: 'admin' },
status: 200,
body: { token: 'fake-jwt-token' },
},
{
matchBody: { username: 'admin', password: 'wrong' },
status: 401,
body: { message: 'Invalid credentials' },
}
)
login(credentials: { username: string; password: string }) {
return this.http.post('/api/login', credentials);
}Simulate network latency
// Global delay (applies to all mocked responses)
MoqModule.forRoot({ delay: 500 });
// Per-scenario delay
@Moq({
status: 200,
body: { data: 'slow response' },
delay: 2000,
})Match by query params
@Moq({
params: { q: 'angular' },
status: 200,
body: { results: [] },
})
search(q: string) {
return this.http.get('/api/search', { params: { q } });
}Side effects on match
@Moq({
status: 200,
body: {},
onMatch: () => {
console.log('User logged out');
},
})⚙️ Configuration
MoqModule.forRoot({
enabled: true, // Master switch. Default: true.
developmentOnly: true, // Only active in dev mode. Default: true.
delay: 0, // Global delay in ms. Default: 0.
logging: false, // Log mocked requests. Default: false.
});| Option | Type | Default | Description |
| ----------------- | --------- | ------- | --------------------------------------------------------------------------- |
| enabled | boolean | true | Master switch. When false, the interceptor is a complete no-op. |
| developmentOnly | boolean | true | When true, the library is active only when isDevMode() returns true. |
| delay | number | 0 | Global delay (ms) applied to all mocked responses. |
| logging | boolean | false | When true, logs each mocked request to the console. |
🏗️ Production Behavior
By default, developmentOnly is true. This means:
- In development (
ng serve): the interceptor is active and mocks requests. - In production (
ng build):isDevMode()returnsfalse, so the interceptor short-circuits and forwards every request to the real network.
This guarantees zero impact on your production bundle behavior — your app behaves exactly as if the library weren't installed.
If you want to force the library to be active in production too (e.g., for a demo build), set developmentOnly: false.
🧩 Standalone API (Angular 14+)
Class-based interceptor (with withInterceptorsFromDi)
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { MoqModule } from 'ng-moq';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptorsFromDi()),
...MoqModule.provideInterceptor({ logging: true }),
],
});Functional interceptor (with withInterceptors)
Use this when you already have functional interceptors in your app:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideMoq, provideMoqProviders } from 'ng-moq';
import { authInterceptor } from './auth.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, provideMoq()])
),
...provideMoqProviders({ logging: true }),
],
});🛠️ How It Works
- The
@Moq(...)decorator runs at class-definition time and stores scenarios in a global registry keyed byClassName.methodName. - The decorator also wraps the original method. When the wrapped method is invoked, it pushes its registry key onto a stack for the duration of the call.
- The
MoqHttpInterceptoris registered viaHTTP_INTERCEPTORSand inspects every outgoing request. - When a request is made from inside a decorated method, the interceptor reads the top of the stack to find the matching scenario — no URL or HTTP method matching is needed. Optional
matchBodyandparamsfilters narrow down which scenario applies when multiple are registered for the same method. - When a scenario matches, the interceptor returns a synthetic
HttpResponse(orHttpErrorResponsefor non-2xx statuses). - When no scenario matches (or the request was not made from a decorated method), the request is forwarded to the next handler unchanged.
📁 Project Structure
ng-moq/
├── projects/
│ └── ng-moq/
│ └── src/
│ ├── public_api.ts
│ └── lib/
│ ├── decorators/
│ │ └── moq.decorator.ts
│ ├── interceptors/
│ │ └── moq-http.interceptor.ts
│ ├── services/
│ │ └── moq-config.service.ts
│ ├── models/
│ │ ├── moq-options.model.ts
│ │ └── moq-scenario.model.ts
│ ├── tokens/
│ │ └── moq.tokens.ts
│ └── module/
│ └── moq.module.ts
├── angular.json
├── package.json
├── tsconfig.json
└── README.md📜 License
MIT
