1 | # Angular in-memory-web-api
|
2 | [![Build Status][travis-badge]][travis-badge-url]
|
3 |
|
4 | An in-memory web api for Angular demos and tests
|
5 | that emulates CRUD operations over a RESTy API.
|
6 |
|
7 | It intercepts Angular `Http` and `HttpClient` requests that would otherwise go to the remote server and redirects them to an in-memory data store that you control.
|
8 |
|
9 | See [Austin McDaniel's article](https://medium.com/@amcdnl/mocking-with-angular-more-than-just-unit-testing-cbb7908c9fcc)
|
10 | for a quick introduction.
|
11 |
|
12 | ### _It used to work and now it doesn't :-(_
|
13 |
|
14 | Perhaps you installed a new version of this library? Check the
|
15 | [CHANGELOG.md](https://github.com/angular/in-memory-web-api/blob/master/CHANGELOG.md)
|
16 | for breaking changes that may have affected your app.
|
17 |
|
18 | If that doesn't explain it, create an
|
19 | [issue on github](https://github.com/angular/in-memory-web-api/issues),
|
20 | preferably with a small repro.
|
21 |
|
22 | ## Use cases
|
23 |
|
24 | * Demo apps that need to simulate CRUD data persistence operations without a real server.
|
25 | You won't have to build and start a test server.
|
26 |
|
27 | * Whip up prototypes and proofs of concept.
|
28 |
|
29 | * Share examples with the community in a web coding environment such as Plunker or CodePen.
|
30 | Create Angular issues and StackOverflow answers supported by live code.
|
31 |
|
32 | * Simulate operations against data collections that aren't yet implemented on your dev/test server.
|
33 | You can pass requests thru to the dev/test server for collections that are supported.
|
34 |
|
35 | * Write unit test apps that read and write data.
|
36 | Avoid the hassle of intercepting multiple http calls and manufacturing sequences of responses.
|
37 | The in-memory data store resets for each test so there is no cross-test data pollution.
|
38 |
|
39 | * End-to-end tests. If you can toggle the app into test mode
|
40 | using the in-memory web api, you won't disturb the real database.
|
41 | This can be especially useful for CI (continuous integration) builds.
|
42 |
|
43 |
|
44 | >**LIMITATIONS**
|
45 | >
|
46 | >The _in-memory-web-api_ exists primarily to support the
|
47 | [Angular documentation](https://angular.io/docs/ts/latest/ "Angular documentation web site").
|
48 | It is not supposed to emulate every possible real world web API and is not intended for production use.
|
49 | >
|
50 | >Most importantly, it is ***always experimental***.
|
51 | We will make breaking changes and we won't feel bad about it
|
52 | because this is a development tool, not a production product.
|
53 | We do try to tell you about such changes in the `CHANGELOG.md`
|
54 | and we fix bugs as fast as we can.
|
55 |
|
56 | ## HTTP request handling
|
57 | This in-memory web api service processes an HTTP request and
|
58 | returns an `Observable` of HTTP `Response` object
|
59 | in the manner of a RESTy web api.
|
60 | It natively handles URI patterns in the form `:base/:collectionName/:id?`
|
61 |
|
62 | Examples:
|
63 | ```ts
|
64 | // for requests to an `api` base URL that gets heroes from a 'heroes' collection
|
65 | GET api/heroes // all heroes
|
66 | GET api/heroes/42 // the hero with id=42
|
67 | GET api/heroes?name=^j // 'j' is a regex; returns heroes whose name starting with 'j' or 'J'
|
68 | GET api/heroes.json/42 // ignores the ".json"
|
69 | ```
|
70 |
|
71 | The in-memory web api service processes these requests against a "database" - a set of named collections - that you define during setup.
|
72 |
|
73 | ## Basic setup
|
74 |
|
75 | <a id="createDb"></a>
|
76 |
|
77 | Create an `InMemoryDataService` class that implements `InMemoryDbService`.
|
78 |
|
79 | At minimum it must implement `createDb` which
|
80 | creates a "database" hash whose keys are collection names
|
81 | and whose values are arrays of collection objects to return or update.
|
82 | For example:
|
83 | ```ts
|
84 | import { InMemoryDbService } from 'angular-in-memory-web-api';
|
85 |
|
86 | export class InMemHeroService implements InMemoryDbService {
|
87 | createDb() {
|
88 | let heroes = [
|
89 | { id: 1, name: 'Windstorm' },
|
90 | { id: 2, name: 'Bombasto' },
|
91 | { id: 3, name: 'Magneta' },
|
92 | { id: 4, name: 'Tornado' }
|
93 | ];
|
94 | return {heroes};
|
95 | }
|
96 | }
|
97 | ```
|
98 |
|
99 | **Notes**
|
100 |
|
101 | * The in-memory web api library _currently_ assumes that every collection has a primary key called `id`.
|
102 |
|
103 | * The `createDb` method can be synchronous or asynchronous.
|
104 | It would have to be asynchronous if you initialized your in-memory database service from a JSON file.
|
105 | Return the database _object_, an _observable_ of that object, or a _promise_ of that object. The tests include an example of all three.
|
106 |
|
107 | * The in-memory web api calls your `InMemoryDbService` data service class's `createDb` method on two occasions.
|
108 |
|
109 | 1. when it handles the _first_ HTTP request
|
110 | 1. when it receives a `resetdb` [command](#commands).
|
111 |
|
112 | In the command case, the service passes in a `RequestInfo` object,
|
113 | enabling the `createDb` logic to adjust its behavior per the client request. See the tests for examples.
|
114 |
|
115 | ### Import the in-memory web api module
|
116 |
|
117 | Register your data store service implementation with the `HttpClientInMemoryWebApiModule`
|
118 | in your root `AppModule.imports`
|
119 | calling the `forRoot` static method with this service class and an optional configuration object:
|
120 | ```ts
|
121 | import { HttpClientModule } from '@angular/common/http';
|
122 | import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
|
123 |
|
124 | import { InMemHeroService } from '../app/hero.service';
|
125 |
|
126 | @NgModule({
|
127 | imports: [
|
128 | HttpClientModule,
|
129 | HttpClientInMemoryWebApiModule.forRoot(InMemHeroService),
|
130 | ...
|
131 | ],
|
132 | ...
|
133 | })
|
134 | export class AppModule { ... }
|
135 | ```
|
136 |
|
137 | **_Notes_**
|
138 |
|
139 | * Always import the `HttpClientInMemoryWebApiModule` _after_ the `HttpClientModule`
|
140 | to ensure that the in-memory backend provider supersedes the Angular version.
|
141 |
|
142 | * You can setup the in-memory web api within a lazy loaded feature module by calling the `.forFeature` method as you would `.forRoot`.
|
143 |
|
144 | * In production, you want HTTP requests to go to the real server and probably have no need for the _in-memory_ provider.
|
145 | CLI-based apps can exclude the provider in production builds like this:
|
146 | ```ts
|
147 | imports: [
|
148 | HttpClientModule,
|
149 | environment.production ?
|
150 | [] : HttpClientInMemoryWebApiModule.forRoot(InMemHeroService)
|
151 | ...
|
152 | ]
|
153 | ```
|
154 |
|
155 | # Examples
|
156 | The tests (`src/app/*.spec.ts` files) in the
|
157 | [github repository](https://github.com/angular/in-memory-web-api/tree/master/src/app)
|
158 | are a good place to learn how to setup and use this in-memory web api library.
|
159 |
|
160 | See also the example source code in the official Angular.io documentation such as the
|
161 | [HttpClient](https://angular.io/guide/http) guide and the
|
162 | [Tour of Heroes](https://angular.io/tutorial/toh-pt6).
|
163 |
|
164 | # Advanced Features
|
165 | Some features are not readily apparent in the basic usage described above.
|
166 |
|
167 | ## Configuration arguments
|
168 |
|
169 | The `InMemoryBackendConfigArgs` defines a set of options. Add them as the second `forRoot` argument:
|
170 | ```ts
|
171 | InMemoryWebApiModule.forRoot(InMemHeroService, { delay: 500 }),
|
172 | ```
|
173 |
|
174 | **Read the `InMemoryBackendConfigArgs` interface to learn about these options**.
|
175 |
|
176 |
|
177 | ## Request evaluation order
|
178 | This service can evaluate requests in multiple ways depending upon the configuration.
|
179 | Here's how it reasons:
|
180 | 1. If it looks like a [command](#commands), process as a command
|
181 | 2. If the [HTTP method is overridden](#method-override), try the override.
|
182 | 3. If the resource name (after the api base path) matches one of the configured collections, process that
|
183 | 4. If not but the `Config.passThruUnknownUrl` flag is `true`, try to [pass the request along to a real _XHR_](#passthru).
|
184 | 5. Return a 404.
|
185 |
|
186 | See the `handleRequest` method implementation for details.
|
187 |
|
188 | ## Default delayed response
|
189 |
|
190 | By default this service adds a 500ms delay
|
191 | to all data requests to simulate round-trip latency.
|
192 |
|
193 | >[Command requests](#commands) have zero added delay as they concern
|
194 | in-memory service configuration and do not emulate real data requests.
|
195 |
|
196 | You can change or eliminate the latency by setting a different `delay` value:
|
197 | ```ts
|
198 | InMemoryWebApiModule.forRoot(InMemHeroService, { delay: 0 }), // no delay
|
199 | InMemoryWebApiModule.forRoot(InMemHeroService, { delay: 1500 }), // 1.5 second delay
|
200 | ```
|
201 |
|
202 | ## Simple query strings
|
203 | Pass custom filters as a regex pattern via query string.
|
204 | The query string defines which property and value to match.
|
205 |
|
206 | Format: `/app/heroes/?propertyName=regexPattern`
|
207 |
|
208 | The following example matches all names start with the letter 'j' or 'J' in the heroes collection.
|
209 |
|
210 | `/app/heroes/?name=^j`
|
211 |
|
212 | >Search pattern matches are case insensitive by default.
|
213 | Set `config.caseSensitiveSearch = true` if needed.
|
214 |
|
215 | <a id="passthru"></a>
|
216 | ## Pass thru to a live server
|
217 |
|
218 | If an existing, running remote server should handle requests for collections
|
219 | that are not in the in-memory database, set `Config.passThruUnknownUrl: true`.
|
220 | Then this service will forward unrecognized requests to the remote server
|
221 | via the Angular default `XHR` backend (it depends on whether your using `Http` or `HttpClient`).
|
222 |
|
223 | <a id="commands"></a>
|
224 | ## Commands
|
225 |
|
226 | The client may issue a command request to get configuration state
|
227 | from the in-memory web api service, reconfigure it,
|
228 | or reset the in-memory database.
|
229 |
|
230 | When the last segment of the _api base path_ is "commands", the `collectionName` is treated as the _command_.
|
231 |
|
232 | Example URLs:
|
233 | ```sh
|
234 | commands/resetdb // Reset the "database" to its original state
|
235 | commands/config // Get or update this service's config object
|
236 | ```
|
237 |
|
238 | Usage:
|
239 | ```sh
|
240 | http.post('commands/resetdb', undefined);
|
241 | http.get('commands/config');
|
242 | http.post('commands/config', '{"delay":1000}');
|
243 | ```
|
244 |
|
245 | Command requests do not simulate real remote data access.
|
246 | They ignore the latency delay and respond as quickly as possible.
|
247 |
|
248 | The `resetDb` command
|
249 | calls your `InMemoryDbService` data service's [`createDb` method](#createDb) with the `RequestInfo` object,
|
250 | enabling the `createDb` logic to adjust its behavior per the client request.
|
251 |
|
252 | In the following example, the client includes a reset option in the command request body:
|
253 | ```ts
|
254 | http
|
255 | // Reset the database collections with the `clear` option
|
256 | .post('commands/resetDb', { clear: true }))
|
257 |
|
258 | // when command finishes, get heroes
|
259 | .concatMap(
|
260 | ()=> http.get<Data>('api/heroes')
|
261 | .map(data => data.data as Hero[])
|
262 | )
|
263 |
|
264 | // execute the request sequence and
|
265 | // do something with the heroes
|
266 | .subscribe(...)
|
267 | ```
|
268 |
|
269 | See the tests for other examples.
|
270 |
|
271 | ## _parseRequestUrl_
|
272 |
|
273 | The `parseRequestUrl` parses the request URL into a `ParsedRequestUrl` object.
|
274 | `ParsedRequestUrl` is a public interface whose properties guide the in-memory web api
|
275 | as it processes the request.
|
276 |
|
277 | ### Default _parseRequestUrl_
|
278 |
|
279 | Default parsing depends upon certain values of `config`: `apiBase`, `host`, and `urlRoot`.
|
280 | Read the source code for the complete story.
|
281 |
|
282 | Configuring the `apiBase` yields the most interesting changes to `parseRequestUrl` behavior:
|
283 |
|
284 | * For `apiBase=undefined` and `url='http://localhost/api/customers/42'`
|
285 | ```ts
|
286 | {apiBase: 'api/', collectionName: 'customers', id: '42', ...}
|
287 | ```
|
288 |
|
289 | * For `apiBase='some/api/root/'` and `url='http://localhost/some/api/root/customers'`
|
290 | ```ts
|
291 | { apiBase: 'some/api/root/', collectionName: 'customers', id: undefined, ... }
|
292 | ```
|
293 |
|
294 | * For `apiBase='/'` and `url='http://localhost/customers'`
|
295 | ```ts
|
296 | { apiBase: '/', collectionName: 'customers', id: undefined, ... }
|
297 | ```
|
298 |
|
299 | **The actual api base segment values are ignored**. Only the number of segments matters.
|
300 | The following api base strings are considered identical: 'a/b' ~ 'some/api/' ~ `two/segments'
|
301 |
|
302 | This means that URLs that work with the in-memory web api may be rejected by the real server.
|
303 |
|
304 | ### Custom _parseRequestUrl_
|
305 |
|
306 | You can override the default parser by implementing a `parseRequestUrl` method in your `InMemoryDbService`.
|
307 |
|
308 | The service calls your method with two arguments.
|
309 | 1. `url` - the request URL string
|
310 | 1. `requestInfoUtils` - utility methods in a `RequestInfoUtilities` object, including the default parser.
|
311 | Note that some values have not yet been set as they depend on the outcome of parsing.
|
312 |
|
313 | Your method must either return a `ParsedRequestUrl` object or `null`|`undefined`,
|
314 | in which case the service uses the default parser.
|
315 | In this way you can intercept and parse some URLs and leave the others to the default parser.
|
316 |
|
317 | ## Custom _genId_
|
318 |
|
319 | Collection items are presumed to have a primary key property called `id`.
|
320 |
|
321 | You can specify the `id` while adding a new item.
|
322 | The service will blindly use that `id`; it does not check for uniqueness.
|
323 |
|
324 | If you do not specify the `id`, the service generates one via the `genId` method.
|
325 |
|
326 | You can override the default id generator with a method called `genId` in your `InMemoryDbService`.
|
327 | Your method receives the new item's collection and collection name.
|
328 | It should return the generated id.
|
329 | If your generator returns `null`|`undefined`, the service uses the default generator.
|
330 |
|
331 | ## _responseInterceptor_
|
332 |
|
333 | You can change the response returned by the service's default HTTP methods.
|
334 | A typical reason to intercept is to add a header that your application is expecting.
|
335 |
|
336 | To intercept responses, add a `responseInterceptor` method to your `InMemoryDbService` class.
|
337 | The service calls your interceptor like this:
|
338 | ```ts
|
339 | responseOptions = this.responseInterceptor(responseOptions, requestInfo);
|
340 | ```
|
341 |
|
342 | <a id="method-override"></a>
|
343 | ## HTTP method interceptors
|
344 |
|
345 | You may have HTTP requests that the in-memory web api can't handle properly.
|
346 |
|
347 | You can override any HTTP method by implementing a method
|
348 | of that name in your `InMemoryDbService`.
|
349 |
|
350 | Your method's name must be the same as the HTTP method name but **all lowercase**.
|
351 | The in-memory web api calls it with a `RequestInfo` object that contains request data and utility methods.
|
352 |
|
353 | For example, if you implemented a `get` method, the web api would be called like this:
|
354 | `yourInMemDbService["get"](requestInfo)`.
|
355 |
|
356 | Your custom HTTP method must return either:
|
357 |
|
358 | * `Observable<Response>` - you handled the request and the response is available from this
|
359 | observable. It _should be "cold"_.
|
360 |
|
361 | * `null`/`undefined` - you decided not to intervene,
|
362 | perhaps because you wish to intercept only certain paths for the given HTTP method.
|
363 | The service continues with its default processing of the HTTP request.
|
364 |
|
365 | The `RequestInfo` is an interface defined in `src/in-mem/interfaces.ts`.
|
366 | Its members include:
|
367 | ```ts
|
368 | req: Request; // the request object from the client
|
369 | collectionName: string; // calculated from the request url
|
370 | collection: any[]; // the corresponding collection (if found)
|
371 | id: any; // the item `id` (if specified)
|
372 | url: string; // the url in the request
|
373 | utils: RequestInfoUtilities; // helper functions
|
374 | ```
|
375 | The functions in `utils` can help you analyze the request
|
376 | and compose a response.
|
377 |
|
378 | ## In-memory Web Api Examples
|
379 |
|
380 | The [github repository](https://github.com/angular/in-memory-web-api/tree/master/src/app)
|
381 | demonstrates library usage with tested examples.
|
382 |
|
383 | The `HeroInMemDataService` class (in `src/app/hero-in-mem-data.service.ts`) is a Hero-oriented `InMemoryDbService`
|
384 | such as you might see in an HTTP sample in the Angular documentation.
|
385 |
|
386 | The `HeroInMemDataOverrideService` class (in `src/app/hero-in-mem-data-override.service.ts`)
|
387 | demonstrates a few ways to override methods of the base `HeroInMemDataService`.
|
388 |
|
389 | The tests ([see below](#testing)) exercise these examples.
|
390 |
|
391 | # Build Instructions
|
392 |
|
393 | Follow these steps for updating the library.
|
394 |
|
395 | - `gulp bump` - up the package version number.
|
396 |
|
397 | - update `CHANGELOG.md` to record the change. Call out _breaking changes_.
|
398 |
|
399 | - update `README.md` if usage or interfaces change.
|
400 |
|
401 | - consider updating the dependency versions in `package.json`.
|
402 |
|
403 | - `npm install` the new package(s) if you did.
|
404 |
|
405 | - `npm list --depth=0` to make sure they really did install!
|
406 |
|
407 | - `gulp clean` to delete all generated files.
|
408 |
|
409 | - `npm test` to dev-build and run tests (see ["Testing"](#testing) below).
|
410 |
|
411 | - `gulp build` to build for distribution.
|
412 |
|
413 | - git add, commit, and push.
|
414 |
|
415 | - `npm publish`
|
416 |
|
417 | - Confirm that angular.io docs samples still work
|
418 |
|
419 | - Add two tags to the release commit in github
|
420 | - the version number
|
421 | - 'latest'
|
422 |
|
423 | [travis-badge]: https://travis-ci.org/angular/in-memory-web-api.svg?branch=master
|
424 | [travis-badge-url]: https://travis-ci.org/angular/in-memory-web-api
|
425 |
|
426 | ## Testing
|
427 |
|
428 | The "app" for this repo is not a real app.
|
429 | It's an Angular data service (`HeroService`) and a bunch of tests.
|
430 |
|
431 | >Note that the `tsconfig.json` produces a `commonjs` module.
|
432 | That's what _Angular specs require_.
|
433 | But when building for an app, it should be a `es2015` module,
|
434 | as is the `tsconfig-ngc.json` for AOT-ready version of this library.
|
435 |
|
436 | These tests are a work-in-progress, as tests often are.
|
437 |
|
438 | The `src/` folder is divided into
|
439 | - `app/` - the test "app" and its tests
|
440 | - `in-mem/` - the source code for the in-memory web api library
|
441 |
|
442 | >A real app would reference the in-memory web api node module;
|
443 | these tests reference the library source files.
|
444 |
|
445 | The `karma-test-shim.js` adds the `in-mem` folder to the list of folders that SystemJS should resolve.
|
446 |
|
447 | ## Rollup
|
448 |
|
449 | The gulp "umd" task runs rollup for tree-shaking.
|
450 |
|
451 | I don't remember if it ever worked without a lot of warnings.
|
452 | The `v0.4.x` release updated to `rollup@0.49` which required updates to the `rollup.config.js`.
|
453 |
|
454 | Still weirdly runs `cjs` rollup config first that I can’t find (which produces numerous warnings) before doing the right thing and running the `umd` config.
|
455 |
|
456 | Also does not work if you follow instructions and use the `output` property of `rollup.config.js`; does work when configure it “wrong” and put the options in the root.
|
457 |
|
458 | Ignoring these issues for now.
|
459 |
|