Cookbook
Recipes for common AngularTS problems
Use the cookbook when you have a concrete problem and need a working pattern.
Start with debugging if AngularTS is new to you. Every progressive recipe ends
with a task to apply and checks that prove it worked.
Find recipes by area
Start with beginner recipes, then choose
forms, HTTP,
DOM updates, security,
performance,
data correctness, navigation,
operations, or
advanced browser work.
Choose who owns the result
Start from the result your application needs, not from a directive name.
Prefer the first applicable row. Do not introduce a client route or shared data
service when one server response and one declarative interaction solve the
problem.
Start here
- Debug a running
application
- Select an element with
ng-el
- Pass server data without another
request
- Update existing DOM from state
- Replace DOM from state
- Swap server-rendered HTML
- Run effects outside the DOM
- Handle events
- Build and submit forms
- Read, write, and prefetch REST data
- Authenticate requests
- Cache REST reads
- Add browser-owned routes
Change server data
Data correctness: handle uniqueness races,
submit money safely,
send dates with zones, and
keep identifiers opaque.
HTTP behavior: revalidate with ETag,
serve maintenance responses,
validate return redirects,
standardize errors, and
negotiate page or fragment.
Browser and content security: sanitize rich text,
deploy CSP,
harden session cookies,
support password managers, and
redact secrets.
Interaction details: focus the first error,
design an empty state, and
preserve focus after swaps.
Transaction workflows: respect rate limits,
run background jobs,
make critical writes idempotent,
save multi-step forms, and
apply bulk actions.
Account and record workflows: archive and restore,
handle permission rejection,
keep filters in the URL, and log out safely.
Production form workflows: show server validation,
send CSRF proof, confirm deletion,
and resolve concurrent edits.
Production response workflows: retry a failed read,
paginate on the server,
download with the browser, and
swap trusted server HTML.
Keep important forms usable with
progressive enhancement, and define their
HTTP response contracts before wiring the success
and failure branches.
Load and navigate faster
Add specialized browser behavior
Integrate another framework
Start with the integration decision guide, then use
the event bus for transient messages,
ng-observe for reflected web-component attributes, or a
shared application model for current reactive state.
Diagnose a production problem
Production change management: deliver server flags,
record audit events, and
roll contracts out compatibly.
When an API must use another origin, follow the
cross-origin request recipe before adding browser
workarounds.
Make failures traceable with a request ID, then run the
shipping check against the complete interaction.
Trace one interaction with the HTTP debugging recipe,
then control repeated requests only after measuring
them. Before shipping,
audit the full server-driven interaction.
1 - Debug a running application
Read and change scopes, controllers, and services from browser DevTools
Problem
A page is running, but you do not know which state, controller, or service owns
the DOM you are looking at. You want to inspect it before changing code.
Before you start
Run a development build with AngularTS debugging helpers available. Open the
page and browser DevTools; no source changes are required for the first
inspection.
Recipe
Open the browser’s Elements panel and select the element you want to
inspect. The DevTools console exposes that element as $0.
Inspect its scope
Select an element that creates a scope, such as an ng-controller, component,
or repeated row. Then run:
const scope = angular.getScope($0);
scope;
angular.getScope() returns
the Scope attached to that compiled
element. Read the same properties used by its template:
scope.query;
scope.results;
Change a value directly:
AngularTS observes writes to the scope proxy and schedules the affected
bindings. You do not need to run a digest. Collection changes work the same way:
scope.results.push({ id: 'debug', name: 'Temporary result' });
Find a scope by ID or name
AngularTS gives each scope a numeric id when it creates it:
scope.id; // for example, 7
scope.root.getById(7);
An ID is useful while inspecting the current page, but it can change after a
reload or after AngularTS recreates part of the page. Give an important scope a
name when you want to find it repeatedly.
Add ng-scope with a non-empty name to the element whose scope you want to
find:
<section ng-controller="CartController" ng-scope="cart">
<p>{{ items.length }} items</p>
</section>
ng-scope="cart" assigns cart to that scope’s scopeName. It does not create
another child scope. The name is required. Find it from the console without
selecting the element again:
const cart = angular.getScopeByName('cart');
cart.items;
cart.refresh();
angular.getScopeByName()
searches the current application’s scope tree.
If the page has several ng-app roots, start with an element inside the
intended application and search from that application’s root scope:
const injector = angular.getInjector($0);
const rootScope = injector.get('$rootScope');
const cart = rootScope.searchByName('cart');
Inspect its controller
angular.getController()
finds the nearest inherited ng-controller controller:
const controller = angular.getController($0);
controller;
controller.reload();
Pass a directive name when you need a specific controller. For example, select
an input with ng-model and run:
const model = angular.getController($0, 'ngModel');
model;
Use this to read controller state and call the same methods as the page.
Inspect available services
angular.getInjector()
walks up from the selected element to the nearest bootstrapped application and
returns its InjectorService:
const injector = angular.getInjector($0);
const rootScope = injector.get('$rootScope');
const http = injector.get('$http');
const router = injector.get('$state');
Use the injector to answer practical questions: whether a service is registered,
which instance the page uses, and what state that instance currently holds.
Choose the right application
A page can contain more than one ng-app. Each application has its own
injector, root scope, and service instances. Start from an element inside the
application you want to inspect.
<main ng-app="storefront">...</main>
<aside ng-app="admin">...</aside>
Get each injector from its own root element:
const storefrontElement = document.querySelector('[ng-app="storefront"]');
const adminElement = document.querySelector('[ng-app="admin"]');
const storefront = angular.getInjector(storefrontElement);
const admin = angular.getInjector(adminElement);
storefront === admin; // false
When $0 is inside storefront, angular.getInjector($0) returns the
storefront injector. When $0 is inside admin, it returns the admin injector.
The same rule applies when reading scopes and controllers: select an element
inside the application you mean to inspect.
Do not fetch a service from one injector and assume it belongs to the other
application. Even when both applications register a service with the same name,
they can hold different instances and state.
Verify
- Select the element whose text or value you want to change.
- Read its scope or controller and find the matching property.
- Change that property in the console.
- Confirm only the expected part of the DOM updates.
- If the page has several
ng-app roots, confirm you selected the intended
one. - Reload the page and confirm the temporary change disappears.
Failure path
Helpers return state from the application containing the selected element. On
pages with several ng-app roots, selecting outside the intended root can
produce a different injector or no result.
Apply it now
Select one element whose behavior you do not understand. Find its application,
scope, and controller in DevTools. Change one reversible value and identify the
exact binding that updates. Keep the scope name only if you expect to inspect
this part of the page again.
2 - Select an element with ng-el
Let AngularTS place a native DOM element on a scope or controller
Problem
Your controller needs a native element so it can focus an input, draw on a
canvas, measure layout, or call another browser API.
Do not search the document with querySelector. Declare the element you need
and let AngularTS wire it to your state.
Before you start
Use this inside a compiled scope or controller that owns the element. Keep
document-wide elements in a service rather than attaching unrelated DOM to a
local scope.
Recipe
Add ng-el with the scope property that should receive the element:
<section ng-controller="StatusController">
<p ng-el="statusEl">Waiting</p>
</section>
AngularTS assigns the native paragraph element to scope.statusEl. Code using
that scope can now change the element directly:
scope.statusEl.textContent = 'Saved';
scope.statusEl.className = 'status status-success';
To try this in DevTools, select the section and run:
const scope = angular.getScope($0);
scope.statusEl.textContent = 'Saved';
scope.statusEl.className = 'status status-success';
There is no selector, element ID, or setup callback to maintain. AngularTS also
removes the reference when the element leaves the DOM.
Store the element on a controller
With controller-as syntax, give ng-el the controller property:
<section ng-controller="StatusController as status">
<p ng-el="status.element">Waiting</p>
</section>
The controller can use this.element:
function StatusController() {
this.markSaved = () => {
this.element.textContent = 'Saved';
this.element.className = 'status status-success';
};
}
Use the element ID as the property name
If ng-el has no value, AngularTS uses the element’s id:
<input id="searchInput" ng-el />
The element is then available as scope.searchInput.
Failure path
The reference is unavailable before the directive links and is removed with the
element. Guard optional elements and do not retain the node in a longer-lived
service.
Apply it now
Search your controller code for one querySelector or getElementById call. If
the element belongs to that controller’s template, replace the search with
ng-el. Verify that the controller no longer knows the page-wide selector and
that the reference disappears when the element is removed.
Verify
Remove and recreate the element. Confirm the reference points to the new node
and no page-wide selector remains.
3 - Pass server data with ng-init or ng-setter
Initialize scope state once or keep it synchronized with server-rendered content
Problem
The server already knows values that the browser needs, such as the current
locale, a record ID, or the initial result count. You want AngularTS to use
those values without making another request.
Before you start
The server template must safely serialize and HTML-encode the initial value.
Decide whether the value is fixed at compilation or can change when another
system replaces element content.
Recipe
Render an ng-init expression with the page:
<main
ng-app="catalog"
ng-init='page = {"locale":"en","productId":42,"resultCount":8}'
>
<p>Found {{ page.resultCount }} products.</p>
<button ng-click="page.resultCount += 1">Add result</button>
</main>
AngularTS evaluates ng-init while it compiles the element. The resulting
page object becomes normal reactive scope state.
The HTML and initial JSON arrive in one server response. This avoids a separate
REST request, its network round trip, and an extra loading state. Use this when
the server already loaded the data while rendering the page.
Inspect it from DevTools by selecting the main element:
const scope = angular.getScope($0);
scope.page.productId;
scope.page.resultCount = 12;
The paragraph updates when resultCount changes.
Keep scope data synchronized with ng-setter
Use ng-setter when the element’s content can change after the initial page
render:
<section ng-app="catalog">
<span ng-setter="availability">In stock</span>
<p>Current availability: {{ availability }}</p>
</section>
AngularTS initially sets scope.availability to 'In stock'. If server-driven
rendering or another integration later changes the span content, AngularTS
updates scope.availability again.
ng-setter observes the element for content changes. It assigns the trimmed
innerHTML string to the expression named by the directive:
<div ng-setter="serverMessage">Order accepted</div>
The scope receives:
scope.serverMessage === 'Order accepted';
Choose between them
| Use | Behavior |
|---|
ng-init | Evaluates an AngularTS expression once during compilation. Use it for JSON objects, numbers, booleans, and other initial values. |
ng-setter | Copies an element’s trimmed HTML into scope as a string and updates it whenever that content changes. |
Both can place server-rendered data in the first HTML response and avoid an
extra REST request. Use ng-setter when later element updates must also reach
the scope.
Render the value on the server
The final HTML sent to the browser should contain the serialized value:
<main ng-init='page = {"locale":"lv","productId":108}'></main>
Use your server framework’s HTML attribute encoder when inserting serialized
data. Do not build the attribute by joining untrusted strings.
Serialize the value to JSON on the server, encode it for an HTML attribute, and
write it into ng-init. The browser can use the data as soon as AngularTS
compiles the page instead of waiting for another endpoint.
Use ng-init for small values already needed by the page. Keep large records in
the server-rendered HTML or load them through an application service when the
user requests them.
Failure path
Malformed or unsafely encoded JSON can break the containing attribute. Encode on
the server and keep large or sensitive records out of HTML attributes.
Apply it now
Find one request the browser makes for data the server already loaded while
rendering the page. Move the smallest useful value into ng-init. If the value
comes from HTML that another system may replace later, use ng-setter and
change the element once in DevTools to confirm the scope follows it.
Verify
Inspect the first document response and confirm the initial value needs no
follow-up request. Change an ng-setter element and confirm scope follows it.
4 - Update text, classes, styles, and visibility
Let bindings update existing DOM when scope state changes
Problem
State changes, and the page must update without finding elements or assigning
DOM properties by hand.
Before you start
Identify the scope or controller property that owns the visible result.
User-provided text must remain text unless it has passed an explicit
trusted-HTML policy.
Recipe
Bind each part of the element to scope state:
<article ng-controller="ProductController as product">
<h2 ng-bind="product.name"></h2>
<p
ng-class="{ featured: product.featured, soldOut: !product.inStock }"
ng-style="{ opacity: product.inStock ? 1 : 0.5 }"
>
{{ product.price | currency }}
</p>
<p ng-show="product.inStock">Available now</p>
<p ng-hide="product.inStock">Out of stock</p>
<button ng-click="product.featured = !product.featured">
Toggle featured
</button>
</article>
Change the model, not the element:
product.name = 'Mechanical keyboard';
product.inStock = false;
AngularTS updates only the bindings that read those properties.
Choose the binding
| Task | Binding |
|---|
| Replace text | ng-bind or {{ expression }} |
| Add and remove classes | ng-class |
| Change inline styles | ng-style |
| Hide while keeping the DOM alive | ng-show or ng-hide |
Use ng-bind-html only for trusted HTML. Normal interpolation and ng-bind
write text, so user content cannot become markup.
Failure path
Do not switch to ng-bind-html to fix formatting. Treat unexpected markup as a
data or formatting problem unless the content has an explicit trust boundary.
Apply it now
Find one place where application code assigns textContent, className,
style, or hidden. Name the state that actually controls that result and bind
the DOM to it. Change the state from DevTools and confirm that no DOM-writing
code remains.
Verify
Change each source property independently. Confirm only its bindings update and
user text is rendered as text rather than HTML.
5 - Replace part of the page from state
Add, remove, or switch DOM subtrees with structural directives
Problem
A page has mutually exclusive steps or an expensive section that should not
exist while it is inactive.
Before you start
Decide whether hidden DOM state must survive. Structural directives destroy
branches; visibility directives keep their controls, media, and browser state
alive.
Recipe
Use ng-switch when one state selects one subtree:
<section ng-switch="checkout.step">
<cart-review ng-switch-when="review"></cart-review>
<payment-form ng-switch-when="payment"></payment-form>
<order-confirmation ng-switch-when="complete"></order-confirmation>
<p ng-switch-default>Loading checkout...</p>
</section>
Change the state normally:
checkout.step = 'payment';
AngularTS removes the old branch, destroys its child scope, creates the new
branch, and compiles its directives.
Use ng-if for one optional subtree:
<address-editor ng-if="editingAddress"></address-editor>
Use ng-include when the replacement comes from a template URL:
<section ng-include="panelTemplate"></section>
Use ng-show instead when the same DOM and local form or media state must
remain alive while hidden.
Failure path
Using ng-if for a frequently toggled form recreates controls and loses local
state. Using ng-show for expensive hidden work keeps that work alive.
Apply it now
Choose one conditional panel in your application. Use ng-show if its form,
media, or local DOM state must survive hiding. Use ng-if if its work should
stop while absent. Use ng-switch if one state chooses among several panels.
Write down which lifecycle behavior you need before changing the directive.
Verify
Toggle the state repeatedly. Confirm hidden state is preserved only with
ng-show and removed branches release their scopes and browser resources.
6 - Swap server-rendered HTML into the page
Select a target and insert, replace, append, or remove server HTML
Problem
An action needs fresh HTML from the server, but it does not justify a client
route or a JSON API followed by client-side rendering.
Before you start
The endpoint must return a fragment rather than a complete document. Choose a
stable target that belongs to the same application and define what remains when
the request fails.
Recipe
Let the server return the final HTML. Select the target with data-target and
choose how to place the response with swap:
<button
ng-get="/account/summary"
data-target="#account-summary"
swap="innerHTML"
>
Refresh summary
</button>
<section id="account-summary">
<p>Current server-rendered summary</p>
</section>
innerHTML keeps the target and replaces its children. Use outerHTML when the
response replaces the target itself:
<button ng-get="/account/card" data-target="#account-card" swap="outerHTML">
Refresh card
</button>
Append server-rendered rows without rebuilding the list:
<button ng-get="/orders?page=2" data-target="#orders" swap="beforeend">
Load more
</button>
<ul id="orders"></ul>
Useful swap values are textContent for plain text, beforeend for append,
afterbegin for prepend, delete for removal, and none when only
on-success should update scope state.
The response HTML is compiled after insertion, so directives in the new fragment
join the same application.
Failure path
A failed response must not erase useful content. Return JSON to on-error or a
deliberate error fragment, and never insert untrusted HTML.
Apply it now
Find one action that fetches JSON only so the browser can rebuild HTML the
server already knows how to render. Return that fragment from the endpoint,
choose the smallest stable target, and select innerHTML, outerHTML, or an
append swap. Test the endpoint with JavaScript disabled before adding the swap.
Verify
Inspect the response content type and target. Confirm inserted directives
compile, failed responses preserve useful UI, and unrelated DOM nodes retain
identity.
7 - Run an effect when state changes
Use scope watches for work outside AngularTS bindings
Problem
A state change must update something outside AngularTS, such as analytics,
storage, a chart, or another browser library.
Before you start
Use an effect only for work outside AngularTS rendering. Identify the scope that
owns the external resource and the operation that releases it.
Recipe
Register a watch for the scope expression that drives the external work:
const stopThemeEffect = scope.watch('settings.theme', (theme) => {
themeStore.save(theme);
chart.setTheme(theme);
});
A watch listener runs after the write, in a microtask. Several synchronous
writes to the same property are coalesced before listeners run:
scope.settings.theme = 'light';
scope.settings.theme = 'dark';
// The effect observes the final value: "dark".
watch returns a deregistration function:
const stop = scope.watch('selectedReport', (report) => {
reportPreview.open(report);
});
scope.on('$destroy', () => {
stop();
reportPreview.destroy();
});
The owning scope removes its watches when it is destroyed. Keep explicit cleanup
when the effect also owns an external resource.
Use bindings for DOM work
Do not use an effect to copy scope values into text, classes, styles, or
visibility. Bind those values with ng-bind, ng-class, ng-style, or
ng-show. Use watch only when the result lives outside AngularTS.
Failure path
Effects can loop when they write the value they watch. Keep the watched input
and external output distinct, and make cleanup safe to call once.
Apply it now
Pick one watcher or state-change callback. If it only changes text, classes,
styles, or visibility, replace it with a binding. If it writes to storage,
analytics, a chart, or another external system, keep it as an effect and name
the resource that must be cleaned up when its scope is destroyed.
Verify
Write the watched value several times synchronously, destroy the scope, and
confirm one final effect runs and all external resources are released.
8 - Handle DOM and scope events
Respond to browser events and communicate through the scope tree
Problem
The page must react to user input, control browser event behavior, or notify
related scopes.
Before you start
Start from a native user interaction and decide whether communication is local
DOM behavior, a short scope notification, or durable shared state.
Handle a DOM event
Use the directive named after the native event:
<button ng-click="cart.add(product)">Add to cart</button>
<input
ng-keydown="$event.key === 'Escape' && dialog.close()"
aria-label="Search"
/>
$event is the native event. Declare event policy on the same element:
<a
href="/checkout"
ng-click="openCheckout()"
data-event-prevent
data-event-once
>
Checkout
</a>
data-event-prevent calls preventDefault(). Other policies are
data-event-stop, data-event-capture, data-event-once, and
data-event-passive.
Send an event through scopes
Register with on and keep the returned cleanup function:
const stop = scope.on('cart:updated', (event, count) => {
scope.cartCount = count;
});
scope.emit('cart:updated', 3);
scope.broadcast('cart:updated', 3);
emit travels to parent scopes. broadcast travels to child scopes. Scope
event listeners run synchronously; reactive DOM updates still flush in the next
microtask.
Use DOM events for element interaction. Use scope events when scopes in the same
application tree need a short-lived notification. Use a service when unrelated
features share durable state.
Failure path
Passive listeners cannot prevent defaults, and broad scope broadcasts can hide
ownership. Keep event policy on the element and use services for lasting state.
Apply it now
Pick one manual addEventListener call. Move it to an ng-* event directive
when it belongs to a template interaction. Use a scope event only when a parent
or child scope needs a short notification. If the value must still exist after
the event, put it in a service instead.
Verify
Use keyboard and pointer input, inspect default prevention, and destroy the
scope. Confirm one handler runs and no listener survives its UI.
9 - Build and validate a form
Bind fields, show useful errors, and submit valid data
Problem
A form needs reactive values, validation messages, submit state, and a server
request without replacing native form behavior unnecessarily.
Before you start
The server endpoint must validate independently and return a deliberate success
or error format. Give every submitted native control a name.
Recipe
Name the form and every control. Bind values with ng-model and use native
validation attributes:
<form
name="signupForm"
ng-post="/api/signup"
on-success="account = $res"
on-error="submitError = $res.message"
loading
loading-class="is-saving"
>
<label for="email">Email</label>
<input id="email" name="email" type="email" ng-model="user.email" required />
<div ng-messages="signupForm.email.error" ng-show="signupForm.email.dirty">
<p ng-message="required">Enter your email.</p>
<p ng-message="email">Enter a valid email.</p>
</div>
<p ng-if="submitError" ng-bind="submitError"></p>
<button type="submit" ng-disabled="signupForm.invalid">Create account</button>
</form>
The form name exposes its controller on scope. Each named ng-model control has
an NgModelController.
Use dirty or touched to avoid showing errors before the user interacts.
Disable submission with invalid, but always validate again on the server.
Submit with ng-post or ng-put
Put ng-post on a form when the request creates something or runs an action.
Use ng-put when it replaces or updates a resource. Both directives:
- Listen for the form’s
submit event. - Prevent the browser from navigating away.
- Collect the current form controls.
- Send the request through
$http. - Run
on-success for a successful response or on-error for a failed one.
The first example sends a JSON object to /api/signup. $res is the parsed
response body. Assign it explicitly in on-success; the directive does not
guess which scope property should receive it.
Use an interpolated URL when updating an existing record:
<form
name="profileForm"
ng-put="/api/profiles/{{ profile.id }}"
on-success="profile = $res"
on-error="saveError = $res.message"
>
<input name="displayName" ng-model="profile.displayName" required />
<button type="submit" ng-disabled="profileForm.invalid">Save</button>
</form>
Choose which controls are sent
The request body comes from the form’s native controls, not by serializing the
whole scope object. Give every submitted control a name. Controls without a
name, disabled controls, and unchecked checkboxes are omitted. This follows the
browser’s FormData rules.
Without enctype, AngularTS converts the collected entries to a JSON object.
Use URL encoding only when the server expects a traditional HTML form body:
<form ng-post="/api/contact" enctype="application/x-www-form-urlencoded">
<input name="email" type="email" ng-model="contact.email" required />
<textarea name="message" ng-model="contact.message" required></textarea>
<button type="submit">Send</button>
</form>
Use the programmatic $http upload shown below for multipart/form-data. The
browser must create that request from the original FormData and add its
boundary; the declarative form directives convert entries to an object.
Handle the request lifecycle
loading sets data-loading="true" on the form while the request is running.
loading-class="is-saving" adds and removes that class at the same time. Use
either as a CSS hook for progress feedback.
Use on-success and on-error when the server returns JSON. If the server
returns HTML, data-target and swap can replace or append server-rendered
content. See Swap server-rendered HTML into the
page.
A login form can submit JSON, show server validation errors, and open a router
state without a controller or a JavaScript submit handler:
<form
ng-post="/_login"
data-state-success="dashboard"
on-error="errors = $res"
ng-init="errors = {}"
>
<label ng-class="{ error: errors.email }">
Email
<input
name="email"
type="email"
aria-invalid="{{ errors.email !== undefined }}"
ng-keyup="errors.email = undefined"
required
/>
<span ng-if="errors.email">{{ errors.email }}</span>
</label>
<label ng-class="{ error: errors.password }">
Password
<input
name="password"
type="password"
aria-invalid="{{ errors.password !== undefined }}"
ng-keyup="errors.password = undefined"
required
/>
<span ng-if="errors.password">{{ errors.password }}</span>
</label>
<button type="submit">Log in</button>
</form>
The inputs do not need ng-model. Their name and current native values are
enough for ng-post to produce this request body:
POST /_login
Content-Type: application/json
{"email":"user@example.com","password":"secret"}
For invalid credentials, return a 4xx status and an application/json body
whose keys match the form fields:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
{"email":"Unknown account","password":"Password does not match"}
AngularTS parses the JSON and exposes it as $res. The inline
on-error="errors = $res" expression makes each message available to the
template. Editing a field clears only that field’s message.
For a 2xx response, AngularTS ignores the error expression and calls
$state.go('dashboard') through data-state-success. The dashboard state
must already be registered with the application’s router. The successful
response may be empty when the next state loads everything it needs.
ng-get and ng-delete can listen to a form submission, but they do not send
the form controls as a request body. Put their parameters in the URL, or use
them on a button. Use ng-post or ng-put when the form fields must be sent.
For search fields, debounce model updates instead of building a timer:
<input
type="search"
ng-model="filters.query"
ng-model-options="{ debounce: 250 }"
/>
Upload a file
A file input writes its native FileList to ng-model. Send the selected
File in native FormData; do not set Content-Type because the browser must
add the multipart boundary.
The fetch transport supports cancellation through a promise-valued timeout,
but it does not currently expose upload progress. Use Cancel a file upload and
handle server rejection for the
complete controller, cancellation, abort, retry, and server-validation pattern.
Failure path
Native and AngularTS validation improve feedback but never replace server
validation. Preserve entered values on 4xx responses and prevent duplicate
destructive submissions.
Apply it now
Take the smallest real form in your application. Give each submitted control a
name, add ng-post or ng-put, and make the server return one realistic 4xx
JSON error body. Confirm the page renders those errors without a controller and
that a 2xx response performs the intended update or router transition.
Verify
Submit valid, invalid, slow, cancelled, and server-rejected requests. Confirm
named controls form the body, errors remain editable, and success performs
exactly one update.
10 - Read and write REST resources
Define a typed endpoint once and use it for CRUD operations
Problem
Several screens call the same endpoint and repeat URL construction, response
mapping, and CRUD request code.
Before you start
Register $rest and $http in the application that owns the data. Define
stable resource IDs and the server methods supported by the endpoint.
Recipe
Create one typed resource with $rest:
interface Task {
id: number;
title: string;
complete: boolean;
}
class TaskRepository {
static $inject = ['$rest'];
private tasks: ng.RestService<Task, number>;
constructor($rest: ng.RestFactory) {
this.tasks = $rest<Task, number>('/api/tasks');
}
list() {
return this.tasks.list();
}
get(id: number) {
return this.tasks.get(id);
}
}
RestService provides list(),
get(), create(), update(), and delete(). Writes stay close to HTTP:
const created = await tasks.create({
title: 'Write release notes',
complete: false,
});
if (!created) throw new Error('Task response was empty');
await tasks.update(created.id, {
...created,
complete: true,
});
await tasks.delete(created.id);
create() sends POST, update() sends PUT, and delete() sends DELETE.
Prefetch data before the user clicks
Start a JSON request when the pointer enters a preview, but keep the result
hidden until the user asks for it:
<div
ng-get="/api/products/42/preview"
data-trigger="mouseenter"
data-throttle="60000"
on-success="preview = $res"
>
<button type="button" ng-click="showPreview = true">Show preview</button>
<div
ng-if="showPreview && !preview"
ng-get="/api/products/42/preview"
data-trigger="load"
on-success="preview = $res"
>
Loading preview...
</div>
<section ng-if="showPreview && preview">
<h3>{{ preview.name }}</h3>
<p>{{ preview.summary }}</p>
</section>
</div>
mouseenter starts the first request without revealing anything. AngularTS
parses the JSON response and on-success stores it on scope. When the user
clicks, ng-click changes only showPreview; if preview is ready, ng-if
renders it immediately without another request.
The inner ng-get is the fallback for keyboard, touch, and fast clicks. It is
created only when the preview is requested but no prefetched value is ready, and
data-trigger="load" starts the request immediately. A click made while the
hover request is still in flight can briefly create a second GET request. Use a
shared cached service when request deduplication matters.
Prefer mouseenter to mouseover for prefetching. mouseover bubbles when the
pointer crosses children and can issue repeated requests. The throttle limits
later pointer-triggered refreshes, but it is not a durable cache. Errors reject
normally, so the caller decides how to render retry and error states.
Failure path
Treat empty create responses, conflicts, cancellation, and stale replies
explicitly. Do not assume every successful write returns an entity body.
Apply it now
Identify data users often need immediately after hovering, focusing, or opening
a panel. Measure the normal click-to-content delay, add intent prefetching, and
measure it again. Decide what touch and keyboard users should trigger, how long
the result may stay fresh, and whether a duplicate in-flight GET is acceptable.
Verify
Inspect request methods and bodies for list, create, update, and delete. Repeat
the prefetched interaction with mouse, keyboard, touch emulation, and a delayed
response.
11 - Authenticate requests
Configure cookie or bearer credentials once for framework services
Problem
Requests need authentication, but controllers should not copy tokens or
credential flags into every call.
Before you start
The server must authenticate and authorize every request. Choose cookie or
bearer transport, define session expiry, and register any router states used
after login.
Recipe
Prefer a server-managed, HttpOnly cookie when the application and API can share
a cookie session:
const session = { currentUser: null };
angular
.createModule('app', [])
.value('session', session)
.config({
$security: {
fallback: 'deny',
isAuthenticated: () => session.currentUser !== null,
credentials: { cookie: true },
},
})
.controller('LoginController', [
'session',
function LoginController(session) {
this.credentials = {};
this.signedIn = (user) => {
session.currentUser = user;
};
},
]);
$http and $rest consult $security before a request and enable browser
credentials when policy allows it. Login remains a normal server request:
<form
name="loginForm"
ng-controller="LoginController as auth"
ng-post="/api/login"
on-success="auth.signedIn($res.user)"
on-error="auth.error = $res.message"
>
<input name="email" type="email" ng-model="auth.credentials.email" required />
<input
name="password"
type="password"
ng-model="auth.credentials.password"
required
/>
<button type="submit" ng-disabled="loginForm.invalid">Log in</button>
<p ng-if="auth.error" ng-bind="auth.error"></p>
</form>
For an API that requires bearer tokens, configure the token source once:
credentials: {
bearer: () => sessionStorage.getItem('accessToken'),
order: ['bearer'],
}
Credential transport and login state are separate. credentials controls what
may be sent; isAuthenticated reports whether the application currently has a
signed-in user. The server remains responsible for authentication and
authorization.
Failure path
Client authentication state is not authorization. Clear local session state
after expiry, avoid token leakage, and let the server reject forbidden
operations.
Apply it now
Write down four facts for your login flow: where credentials are submitted,
which cookie or token proves later requests, how the client learns that login
succeeded, and which router state opens next. Then test invalid credentials, an
expired session, and a direct visit to a protected URL rather than testing only
the successful form.
Verify
Test login, logout, expiry, direct private navigation, and a forbidden action.
Confirm credentials use the intended transport and server authorization remains
decisive.
12 - Cache REST reads
Add cache-first, network-first, or stale-while-revalidate behavior
Problem
Reference data is requested repeatedly even though it changes infrequently.
Before you start
Provide a RestCacheStore
implementation and classify the response as public, user-specific, mutable, or
immutable before selecting a policy.
Recipe
Start with a cache store. This in-memory version lasts until the page reloads:
class MemoryRestCache {
entries = new Map();
async get(key) {
return this.entries.get(key);
}
async set(key, value) {
this.entries.set(key, value);
}
async delete(key) {
this.entries.delete(key);
}
async deletePrefix(prefix) {
for (const key of this.entries.keys()) {
if (key.startsWith(prefix)) this.entries.delete(key);
}
}
}
const appCache = new MemoryRestCache();
The object implements
RestCacheStore. Replace it
with IndexedDB or another persistent store when cached data must survive a page
reload. Then wrap the HTTP REST backend:
import {
CachedRestBackend,
HttpRestBackend,
} from '@angular-wave/angular.ts/services/rest';
const backend = new CachedRestBackend({
network: new HttpRestBackend($http),
cache: appCache,
policy: ({ url }) =>
url.startsWith('/api/catalog') ? 'stale-while-revalidate' : 'network-first',
});
const products = $rest<Product, number>('/api/catalog/products', Product, {
backend,
});
CachedRestBackend accepts
any RestCacheStore. Use
memory for one-page reuse, IndexedDB for durable structured data, or the Cache
API for HTTP-style storage.
Choose the strategy by what stale data means:
| Strategy | Use it when |
|---|
cache-first | Data is effectively immutable and startup speed matters most. |
network-first | Fresh data matters, but cached data is a useful offline fallback. |
stale-while-revalidate | Showing cached data now is better than waiting for a refresh. |
Writes always go to the network first. Successful writes invalidate cached
collection and entity keys for that resource.
Failure path
The wrong policy can expose another user’s data or preserve an invalid value.
Partition private caches and invalidate after every relevant write.
Apply it now
Open the Network panel and find a GET that repeats during one normal task. Pick
a policy from the consequence of stale data: use cache-first for an immutable
country list, network-first for active orders, or stale-while-revalidate for
a product catalog. Perform a write and confirm the affected collection and
entity entries are invalidated. Never share user-specific cached responses
between signed-in users.
Verify
Compare network requests before and after caching, then perform a write and an
offline read. Confirm invalidation and stale behavior match the selected policy.
13 - Add application routes
Register states, render them in ng-view, and protect private pages
Problem
A client-owned area needs URLs, back-button support, nested views, and route
data. A normal server page is no longer enough for that area.
Before you start
Use client routing only inside an area the browser must own. The server must
still serve the initial application URL and authentication endpoints.
Recipe
Register states on the module:
const accountRoutes = [
{
name: 'account',
url: '/account',
component: 'accountHome',
},
{
name: 'account.orders',
url: '/orders',
component: 'orderList',
policy: {
navigation: { authenticated: true, redirectTo: 'login' },
},
},
{
name: 'account.order',
url: '/orders/:id',
component: 'orderDetail',
policy: {
navigation: { authenticated: true, redirectTo: 'login' },
},
},
] as const satisfies readonly ng.RouterModuleDeclaration[];
angular.createModule('account', []).router(accountRoutes);
Render the active state and create links:
<nav>
<a ng-state="'account'">Account</a>
<a ng-state="'account.orders'">Orders</a>
</nav>
<main ng-view></main>
Use $state.go() when code initiates navigation:
$state.go('account.orders');
$state.go('account.order', { id: order.id });
StateService also provides
href() for URL generation and matches() for active-state checks.
Keep the first page server-rendered. Add a route only when the browser must own
navigation and state across several related views.
Failure path
A route that works only after visiting another state is incomplete. Resolve
direct URLs, rejected policy, missing data, and superseded navigation.
Apply it now
List the URLs in one browser-owned area. Keep pages on the server unless users
need client-side back-button history and state across those URLs. Register the
smallest useful parent and child states, open a child URL directly, refresh it,
and use Back. If any step depends on first visiting another state, the route is
not complete.
Verify
Open every documented URL directly, refresh it, move through child states, and
use Back and Forward. Confirm private states enforce policy before rendering.
14 - Load content when it enters the viewport
Delay below-the-fold work with ng-viewport
Problem
A chart, comment list, or request should not start until the user scrolls near
it.
Before you start
Measure the initial request or CPU work first. The deferred feature must have a
visible loading or empty state when it has not started.
Recipe
Use ng-viewport and run the enter expression once:
<section
ng-viewport
data-viewport-once
data-viewport-margin="200px 0px"
on-enter="comments.load()"
>
<p ng-if="comments.loading">Loading comments...</p>
<comment-list ng-if="comments.ready"></comment-list>
</section>
The margin starts work 200 pixels before the section enters the viewport.
data-viewport-once stops later enter events after the first one.
Use on-leave when work should pause after leaving the viewport:
<video ng-viewport on-enter="player.play()" on-leave="player.pause()"></video>
The directive owns its browser observer and disconnects it when the scope is
destroyed.
Failure path
Intersection is only a scheduling hint. Keep keyboard access and an explicit
retry path, and disconnect observers when their scope is destroyed.
Apply it now
Record the requests and long tasks made before first paint. Choose one
below-the-fold feature that is not needed yet, move its start expression to
on-enter, and set the margin from measured load time rather than guessing.
Scroll past it twice and confirm data-viewport-once prevents duplicate work.
Verify
Scroll into and out of the element twice, then remove it. Confirm the margin,
once behavior, leave behavior, and observer cleanup match the recipe.
15 - Receive live server events
Update scope or append HTML through Server-Sent Events
Problem
The server must push notifications or status updates to the page without client
polling.
Before you start
The server must support text/event-stream, flush events, and handle
reconnects. Decide what the page shows before the first event arrives.
The browser side uses SseService
through ng-sse. AngularTS owns the
SseConnection, reconnects
it, and closes it when the scope is destroyed.
For a custom production runtime, include sseModule from
@angular-wave/angular.ts/runtime/realtime. Do not include the aggregate
realtimeModule unless the same runtime also uses WebSocket and WebTransport.
Set up a JavaScript SSE endpoint
An SSE endpoint is a long-running HTTP GET. Send the event-stream headers, end
every event with a blank line, flush each update, and stop work when the request
closes.
import { createServer } from 'node:http';
const states = ['paid', 'packing', 'shipped'];
createServer((request, response) => {
if (request.url !== '/api/events/orders') {
response.writeHead(404).end();
return;
}
response.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
});
response.flushHeaders();
let index = 0;
const send = () => {
const order = { id: 'A-1042', state: states[index++ % states.length] };
response.write(`data: ${JSON.stringify(order)}\n\n`);
};
const updates = setInterval(send, 15_000);
const heartbeat = setInterval(
() => response.write(': keep-alive\n\n'),
10_000,
);
send();
request.on('close', () => {
clearInterval(updates);
clearInterval(heartbeat);
});
}).listen(8080);
The blank line after data: completes one event. Lines beginning with : are
comments that keep idle proxies from closing the connection. In production,
publish real order changes rather than using timers.
Authenticate and authorize before writing the 200 response. Browser
EventSource cannot attach arbitrary headers, so prefer an HttpOnly,
Secure, SameSite session cookie and with-credentials="true". Do not put a
long-lived bearer token in the URL.
Send complete current-state snapshots when reconnecting is enough. If every
event must be delivered, add an id: field, store events durably, read the
browser’s Last-Event-ID header on reconnect, and replay events after that ID
before sending live ones. A process-local counter is not sufficient when more
than one server can handle the request.
Recipe
Use ng-sse and assign incoming JSON from $res:
<section
ng-sse="/api/events/orders"
trigger="load"
with-credentials="true"
on-success="orderStatus = $res; streamState = 'live'"
on-error="streamState = 'reconnecting'"
on-reconnect="streamState = 'live'"
>
<p aria-live="polite">Order {{ orderStatus.id }}: {{ orderStatus.state }}</p>
<p ng-if="streamState === 'reconnecting'">Reconnecting...</p>
</section>
When the server sends HTML fragments, choose a swap strategy:
<ul
id="notifications"
ng-sse="/api/events/notifications"
trigger="load"
swap="beforeend"
></ul>
AngularTS parses JSON for on-success and otherwise applies the configured DOM
swap. Keep each message small. Send an identifier and changed fields, or a small
current-state snapshot, instead of an entire page model.
Use SSE for one-way server updates. Use a WebSocket or WebTransport service when
the browser and server both need a long-lived message channel.
Failure path
SSE is one-way and connections can disappear through proxies, deployments, or
sleep. Show stale or disconnected state and make repeated events safe. Confirm
that your reverse proxy disables response buffering and permits connections to
remain open. Keep polling when your hosting path cannot stream responses.
Apply it now
Find one polling loop that only asks whether the server has new data. Write the
smallest event payload the server can push, replace the poll with SSE, and show
what the user sees before the first event and after the connection fails. Keep
polling when intermediaries cannot maintain long-lived HTTP connections.
Verify
Send valid JSON, HTML, malformed data, and a disconnect. Confirm the page shows
its initial, live, error, and reconnect states without polling. Reconnect with a
Last-Event-ID, test the page through the production proxy, and confirm the
server releases timers and subscriptions when the browser disconnects.
16 - Move CPU-heavy work off the main thread
Run JavaScript in a Web Worker with ng-worker
Problem
Sorting, compression, or data processing blocks input and rendering on the main
browser thread.
Before you start
Profile a realistic input and define a serializable message contract. Browser
APIs and DOM nodes cannot be passed to the worker as ordinary values.
Recipe
Point ng-worker at a worker file and bind its result:
<button
ng-worker="/workers/sort.js"
data-params="{ items: products }"
on-result="sortedProducts = $result"
on-error="sortError = error"
trigger="click"
>
Sort products
</button>
<ul>
<li ng-repeat="product in sortedProducts">{{ product.name }}</li>
</ul>
The worker receives data-params through postMessage:
self.onmessage = (event) => {
const sorted = event.data.items
.slice()
.sort((left, right) => left.name.localeCompare(right.name));
self.postMessage(sorted);
};
AngularTS assigns the reply to $result and updates the scope expression in
on-result. The directive terminates its worker when its scope is destroyed.
Use data-request with a registered shared worker when several callers need
correlated request and response messages.
Failure path
Worker startup and cloning can cost more than the task. Ignore stale replies,
terminate owned workers, and keep errors visible in the owning UI.
Apply it now
Profile the interaction before adding a worker. Move work only when a measured
CPU task blocks the main thread long enough to affect input or rendering. Define
a small serializable request and response, test the largest realistic input, and
decide how a newer request cancels or ignores an older result.
Verify
Compare the main-thread trace before and after moving the task. Confirm input
stays responsive, stale replies are ignored, and the worker terminates with its
scope.
17 - Load a WebAssembly module
Expose WebAssembly exports to scope with ng-wasm
Problem
A calculation already exists as WebAssembly and the page needs to call its
exports without writing module-loading lifecycle code.
Before you start
Have a deployed WebAssembly module and know its exported function contract. The
server must send the correct MIME type.
Recipe
Load the module with ng-wasm and name its scope resource with as:
<div ng-wasm src="/wasm/shipping.wasm" as="shipping"></div>
<p ng-if="shipping.status === 'loading'">Loading shipping calculator...</p>
<p ng-if="shipping.status === 'error'">Shipping estimate is unavailable.</p>
<p ng-if="shipping.status === 'ready'">
Shipping: {{ shipping.exports.quote(cart.weightGrams, destination.zone) }}
cents
</p>
AngularTS exposes a
WasmResource as shipping.
Check status before calling the pricing export. The cart and destination stay
in normal reactive scope state.
The server must serve the module with the correct WebAssembly MIME type. The
directive owns loading and releases its resource when the scope is destroyed.
Keep DOM work in AngularTS. Use WebAssembly for compute-heavy functions with a
small, explicit input and output contract.
Failure path
Module loading, memory copies, and MIME failures can erase compute gains. Keep a
JavaScript or server fallback when the calculation is required.
Apply it now
Choose WebAssembly only for an existing compute-heavy algorithm, not ordinary UI
arithmetic. Measure the JavaScript version, module startup, and data-transfer
cost with realistic inputs. Keep the module only when repeated calls recover its
loading cost and the input/output boundary stays small.
Verify
Test loading, MIME failure, realistic calculations, repeated calls, and scope
removal. Confirm the measured gain exceeds module and transfer costs.
18 - Edit a server record in place
Load an editor, submit JSON, and keep server validation beside the field
Problem
A user should edit one record without leaving the page. The server owns
validation and returns the saved record.
Before you start
The page already has the record ID and summary. The endpoint accepts JSON and
returns field errors with status 422.
Working example
<article
ng-init='profile = {"id":42,"name":"Ada"}; editing = false; errors = {}'
>
<p ng-if="!editing">{{ profile.name }}</p>
<button ng-if="!editing" ng-click="editing = true">Edit name</button>
<form
ng-if="editing"
ng-put="/api/profiles/{{ profile.id }}"
on-success="profile = $res; editing = false; errors = {}"
on-error="errors = $res"
>
<input name="name" ng-model="profile.name" required />
<span ng-if="errors.name">{{ errors.name }}</span>
<button type="submit">Save</button>
<button type="button" ng-click="editing = false">Cancel</button>
</form>
</article>
Server contract
PUT /api/profiles/42
Content-Type: application/json
{"name":"Ada Lovelace"}
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
{"name":"That name is already used"}
What AngularTS wires
The form creates JSON from named controls. A 2xx response replaces profile;
a 4xx response becomes $res and stays beside the editor.
Failure path
Keep the editor open after validation or network failure. Do not replace the
user’s input with the old server value until they cancel.
Apply it now
Choose one record users currently open on another page just to change one or two
fields. Define its update URL, successful JSON body, and one realistic 422
body.
Verify
Edit, reject, correct, and save the record. Confirm focus and entered values
survive the rejected request and the final summary uses the server response.
19 - Delete a row without rebuilding its list
Let the server authorize deletion and remove only the successful row
Problem
Deleting one item should not recreate the list, lose scroll position, or remove
the row before the server authorizes the action.
Before you start
The server must authorize deletion and return success only after the record is
gone. Give the rendered row a stable DOM ID that is unique on the page.
Working example
<ul>
<li id="task-42">
<span>Send invoice</span>
<button
ng-delete="/api/tasks/42"
data-target="#task-42"
swap="delete"
on-error="deleteError = $res.message"
>
Delete
</button>
</li>
</ul>
<p ng-if="deleteError">{{ deleteError }}</p>
Server contract
Return 204 No Content only after authorization and deletion succeed. Return a
403 or 409 JSON body such as {"message":"This task is locked"} when the
row must remain.
What AngularTS wires
The click sends DELETE. The delete swap removes the selected row only after
a successful response. Errors update scope instead of touching the list.
Failure path
Disable or mark the button while the request runs when duplicate deletion
matters. Always enforce ownership and authorization on the server.
Apply it now
Find a delete action that refreshes an entire collection. Give each row a stable
DOM ID and make the endpoint return an error that explains why deletion can
fail.
Verify
Delete the middle row, then reject another deletion. Confirm sibling nodes,
focus, scroll position, and the rejected row remain unchanged.
20 - Search while the user types
Debounce requests and ignore responses for an older query
Problem
Search should feel immediate without sending a request for every keystroke or
letting an older response replace newer results.
Before you start
The search endpoint must accept a bounded query and echo its normalized query in
the JSON response. Decide the minimum useful query length and result limit.
Working example
<section ng-init="query = ''; results = []">
<input
type="search"
name="query"
ng-model="query"
ng-get="/api/search?q={{ query }}"
data-trigger="input"
data-delay="250"
on-success="$res.query === query && (results = $res.items)"
/>
<p ng-if="query && !results.length">No results for {{ query }}.</p>
<ul>
<li ng-repeat="result in results">{{ result.name }}</li>
</ul>
</section>
Server contract
Echo the normalized query with the results: {"query":"boots","items":[...] }.
The equality check prevents a slow response for an earlier query from winning.
What AngularTS wires
data-delay waits for a pause. URL interpolation reads the current model, and
on-success applies only the response that still matches it.
Failure path
Render request errors separately from an empty result. Add a minimum query
length when broad searches are expensive, and enforce limits on the server.
Apply it now
Pick the search box with the highest request count. Record requests per query
and time to useful results before and after adding the delay and stale-response
check.
Verify
Type two queries quickly and delay the first response in DevTools. Confirm the
URL contains the latest query and the late response never replaces newer
results.
21 - Load one select from another
Request valid choices when a parent selection changes
Problem
A city, model, or category list depends on an earlier selection and should not
be loaded before it is needed.
Before you start
The parent values must use stable server identifiers. The child endpoint must
validate the parent identifier and return only choices valid for it.
Working example
<form
ng-init="countries = [{code:'LV', name:'Latvia'}, {code:'EE', name:'Estonia'}]; cities = []"
>
<select
name="country"
ng-model="country"
ng-options="item.code as item.name for item in countries"
ng-get="/api/cities?country={{ country }}"
on-success="cities = $res; city = undefined"
></select>
<select
name="city"
ng-model="city"
ng-options="item.id as item.name for item in cities"
ng-disabled="!cities.length"
></select>
</form>
Server contract
GET /api/cities?country=LV returns a bounded JSON array such as
[{"id":1,"name":"Riga"}]. Reject unknown country codes on the server.
What AngularTS wires
A select uses change as its default HTTP trigger. The response replaces the
available cities and clears a city that belonged to the previous country.
Failure path
Clear dependent values before showing new choices. Show a retry message when the
request fails instead of leaving stale choices enabled.
Apply it now
Find a form that initially downloads every possible option. Move one dependent
list behind its parent selection and compare initial payload size.
Verify
Change the parent twice. Confirm the child value clears, stale choices cannot be
submitted, and the request includes only an allowed parent identifier.
22 - Load the next page near the viewport
Append server-rendered rows and replace the paging marker
Problem
A long feed should load another server-rendered page shortly before the user
reaches the end without rebuilding existing rows.
Before you start
The server must expose normal paginated URLs and return HTML fragments with
stable row identity. Keep explicit pagination as the non-JavaScript and keyboard
fallback.
Working example
<ul id="orders">
<li>Order 1001</li>
<li>Order 1002</li>
</ul>
<div
id="next-orders"
ng-viewport
data-viewport-once
data-viewport-margin="300px 0px"
on-enter="nextPage = 2"
ng-get="/orders?page={{ nextPage }}"
data-latch="{{ nextPage }}"
data-target="#next-orders"
swap="outerHTML"
>
Loading more orders...
</div>
Server contract
Return new <li> rows followed by the same marker configured for page 3. Omit
the marker on the last page. Keep a normal paginated URL so refresh and sharing
work.
What AngularTS wires
The viewport changes nextPage; the latch sends the request; outerHTML
replaces the old marker with rows and the next marker. Existing rows stay alive.
Failure path
Return a retry button when loading fails. Do not trap keyboard users in an
endless feed; provide a normal pagination link and a reachable page footer.
Apply it now
Choose one feed where users commonly request page 2. Measure its rendered row
count, DOM size, and requests before replacing the current pagination behavior.
Verify
Scroll with the Network panel open. Confirm each page loads once, rows are not
recreated, the final response removes the marker, and the fallback URL still
works.
23 - Update immediately and roll back on failure
Show a local change first while preserving the previous server value
Problem
A reversible action such as completing a task should feel immediate, but a
rejected request must restore the last confirmed value.
Before you start
Choose a reversible, low-risk action and retain the last confirmed server value.
The endpoint must return the canonical value or a clear conflict response.
Working example
<form
ng-init='task = {"id":42,"complete":false}; confirmed = false'
ng-put="/api/tasks/{{ task.id }}"
on-success="task = $res; confirmed = $res.complete; saveError = undefined"
on-error="task.complete = confirmed; saveError = $res.message"
>
<label>
<input name="complete" type="checkbox" ng-model="task.complete" />
Complete
</label>
<button type="submit">Save</button>
<span ng-if="saveError">{{ saveError }}</span>
</form>
Server contract
Return the canonical task after success. Return 409 Conflict with
{"message":"Task changed on another device"} when the submitted version is
stale.
What AngularTS wires
The checkbox updates local rendering immediately. confirmed changes only after
a successful response and supplies the rollback value after failure.
Failure path
Use optimism only for reversible changes. Payments, destructive operations, and
permission changes should wait for server confirmation.
Apply it now
Choose one low-risk action that currently waits on the network. Name the
confirmed value, the optimistic value, and the exact response that requires
rollback.
Verify
Test success, offline failure, and conflict. Confirm rollback uses the last
confirmed server value rather than a guessed default.
24 - Return users to login after a session expires
Route a failed private request and preserve the intended destination
Problem
A user opens a private action after their cookie session expires. The
application must show login rather than leaving a broken panel.
Before you start
Register the login router state and configure the request credential transport.
Decide how the login flow stores and restores the intended destination.
Working example
<section>
<button
ng-get="/api/account"
on-success="account = $res"
on-error="sessionError = $res.message"
data-state-error="login"
>
Refresh account
</button>
<p ng-if="sessionError">{{ sessionError }}</p>
</section>
Server contract
The private endpoint returns 401 with {"message":"Your session expired"}.
The login state records the intended state and returns there after successful
login.
What AngularTS wires
The directive exposes the JSON error and asks the router to enter login.
Configure cookie credentials and route policy once; do not add auth logic to
every button.
Failure path
data-state-error runs for every failed status, not only 401. Use this direct
pattern only for an endpoint whose failures all require login; otherwise
centralize status-aware handling in the HTTP or security layer.
Apply it now
Expire a real session, then activate one private request and one private URL.
Decide where the intended destination is stored and which failures should not
route to login.
Verify
Confirm the login page appears, credentials are not placed in URLs or logs, and
a successful login returns to the original destination exactly once.
25 - Cache reference data without sharing user data
Cache stable lookup lists and invalidate them deliberately
Problem
Country, currency, or category lists are downloaded repeatedly even though they
change rarely and are identical for every user.
Before you start
Provide an application cache implementing the linked cache-store contract.
Confirm the endpoint is independent of user identity before sharing entries.
Working example
import {
CachedRestBackend,
HttpRestBackend,
} from '@angular-wave/angular.ts/services/rest';
const referenceBackend = new CachedRestBackend({
network: new HttpRestBackend($http),
cache: appCache,
policy: ({ url }) =>
url.startsWith('/api/reference/') ? 'cache-first' : 'network-first',
});
const countries = $rest<Country, string>('/api/reference/countries', Country, {
backend: referenceBackend,
});
<label>
Country
<select
ng-model="shipping.country"
ng-options="country.code as country.name for country in countries"
></select>
</label>
The cached result is ordinary application state. Bind it like any other list;
the template does not need to know which cache policy supplied it.
Server contract
The endpoint returns stable IDs, supports conditional HTTP caching, and never
varies by the signed-in user. Publish a version change when administrators
update the list.
What AngularTS wires
The cached backend reads through the supplied
RestCacheStore. Successful
writes invalidate matching collection and entity entries automatically.
Failure path
Do not place account, permission, price, or personalized responses in a shared
cache. Use network-first when stale data can block a current operation.
Apply it now
Find a repeated GET in the Network panel. Prove its response is
user-independent, choose an acceptable stale lifetime, and identify the write
that invalidates it.
Verify
Load the same form twice, test offline fallback, update the reference list, and
sign in as another user. Confirm the cache never crosses a user-specific
boundary.
26 - Cancel a file upload and handle server rejection
Send FormData, abort through timeout, and distinguish cancellation
Problem
A large upload needs cancellation and useful server errors. The current fetch
transport does not expose upload progress events.
Before you start
Register the controller on the application module and attach it to the form. Set
server-side size, content, authorization, and storage limits before accepting
files.
Working example
class UploadController {
static $inject = ['$http'];
constructor($http) {
this.$http = $http;
}
async send(file) {
this.cancellation = Promise.withResolvers();
const body = new FormData();
body.append('file', file);
try {
await this.$http.post('/api/uploads', body, {
timeout: this.cancellation.promise,
});
} catch (error) {
if (error.xhrStatus !== 'abort') this.error = error.data.message;
}
}
cancel() {
this.cancellation?.resolve();
}
}
<form
ng-controller="UploadController as upload"
ng-submit="upload.send(upload.files[0])"
>
<input type="file" name="file" ng-model="upload.files" required />
<button type="submit">Upload</button>
<button type="button" ng-click="upload.cancel()">Cancel</button>
<p ng-if="upload.error">{{ upload.error }}</p>
</form>
Server contract
Validate size, MIME signature, filename, authorization, and storage limits on
the server. Return 413 for size limits and 422 JSON for rejected content.
What AngularTS wires
Resolving the promise supplied as timeout aborts the underlying request. An
aborted request rejects with xhrStatus === 'abort'.
Failure path
Do not set Content-Type manually for FormData; the browser adds the
multipart boundary. Do not advertise progress: uploadEventHandlers is ignored
by the fetch transport today.
Apply it now
Choose the largest upload your application accepts. Define its limit,
cancellation behavior, retry behavior, and the message returned for each server
rejection.
Verify
Cancel during transfer, reject an oversized file, and retry a valid file.
Confirm an abort shows no failure message and no partial object becomes
accessible.
27 - Load a dialog from the server
Swap server-rendered dialog content and restore focus on close
Problem
A confirmation dialog needs current server data, but its trigger must keep focus
and remain usable if loading fails.
Before you start
The trigger must remain in the DOM while the fragment is open. The server
fragment owns its accessible title, initial focus, close behavior, and
authorization result.
Working example
<button
ng-el="dialogTrigger"
ng-get="/account/delete-dialog"
data-target="#dialog-host"
swap="innerHTML"
on-error="dialogError = $res.message"
>
Delete account
</button>
<div id="dialog-host"></div>
<p ng-if="dialogError">{{ dialogError }}</p>
Server contract
Return a labelled dialog fragment with a focused cancel button. Its close action
calls dialogTrigger.focus() before removing the fragment. Return 403 JSON
when the current user may no longer perform the action.
What AngularTS wires
The response is inserted and compiled in the trigger’s scope, so the fragment
can use dialogTrigger, event directives, validation, and another HTTP
directive.
Failure path
Keep the trigger in place while loading and failure occur. The fragment must
trap focus when modal, close on Escape, restore focus, and expose a visible
title.
Apply it now
Choose one modal whose content is stale or duplicated in client code. Move its
title, warning, and authorization decision to one fragment endpoint.
Verify
Open by keyboard, cancel, reopen, reject authorization, and complete the action.
Confirm focus always returns to the trigger and the background is not
interactive.
28 - Understand the similarity to HTMX
Compare the server-first approaches without assuming compatibility
Problem
AngularTS HTTP directives look familiar if you know HTMX. You want to understand
the shared approach without assuming the APIs behave alike.
Before you start
Start with the AngularTS HTTP directive reference. Treat HTMX knowledge as
background for the server-first design, not as documentation for AngularTS.
Similar design questions
| Design question | AngularTS mechanism |
|---|
| Trigger a request | ng-get, ng-post, ng-put, ng-delete, data-trigger |
| Choose the destination | data-target |
| Choose insertion behavior | swap |
| Render a server fragment | Return HTML; AngularTS inserts and compiles it |
| Read JSON | Assign $res in on-success or on-error |
| Show request state | loading, loading-class, bindings |
| Navigate after success | data-state-success |
| Prefetch from intent | Event trigger plus hidden scope state |
| Lazy load near the viewport | ng-viewport plus an HTTP directive |
| Keep local interaction state | Scope, controller, or typed component view |
Where the approach feels similar
Both approaches encourage useful questions for click-to-edit, active search,
polling, lazy loading, server validation, fragment swaps, pagination, and
prefetching: who renders the result, what triggers the request, where it goes,
and what failure looks like.
Do not infer compatibility
AngularTS does not promise HTMX syntax or behavior. It has its own attributes,
scope expressions, lifecycle, router, security configuration, response handling,
and tests. Similar names or outcomes do not make an HTMX example an AngularTS
example.
Failure path
Do not copy an HTMX attribute, extension, event, header, or history option and
assume AngularTS recognizes it. Build from the AngularTS reference, preserve the
normal server endpoint, and test error and navigation behavior independently.
Apply it now
Choose one server interaction in your application. Define its trigger, request,
response type, target, local state, fallback, and failure behavior using only
AngularTS documentation. Then note which design questions happen to feel
familiar from HTMX.
Verify
Test the normal link or form fallback, direct server endpoint, successful swap
or JSON assignment, failed response, keyboard path, and browser history. Do not
use an HTMX result as evidence that AngularTS behaves the same way.
29 - Design an HTTP response for the UI
Choose status, content type, and body before writing the directive
Problem
A directive can only handle success, validation, navigation, and swaps correctly
when the server response has a deliberate contract.
Before you start
Choose whether the server or browser renders the result. Write down what the user
should see after success, invalid input, expired authentication, conflict, and
unexpected failure.
Choose the response
| Situation | Response contract |
|---|
| Saved data is needed locally | 2xx application/json with the canonical saved record |
| Only success matters | 204 No Content |
| Server owns the replacement UI | 2xx text/html with one fragment |
| User can correct submitted values | 422 application/json keyed by field name |
| Authentication expired | 401 application/json with a safe user message |
| User lacks permission | 403 application/json |
| Submitted version is stale | 409 application/json with conflict details |
| Server failed unexpectedly | 5xx application/json with a public message ID |
Use on-success or on-error for JSON. Use data-target and swap for HTML.
Do not return HTML error pages to a JSON interaction or 200 for rejected form
data merely to avoid the error path.
Failure path
Never expose stack traces, database errors, tokens, or internal identifiers in a
public error body. Log a correlation ID on the server and return only that ID
with a safe message.
Apply it now
Choose one endpoint used by a cookbook recipe. Write its method, URL, success
status, success content type, failure statuses, and one real body for each path
before changing the template.
Verify
Inspect every response in the Network panel. Confirm its status and content type
match the body, an empty success is actually empty, and each failure reaches the
intended UI path.
30 - Make a server-driven interaction accessible
Preserve keyboard behavior, announce loading, and move focus deliberately
Problem
A request updates the page visually, but keyboard and screen-reader users also
need an operable trigger, announced status, and predictable focus.
Before you start
Use a native button or link for the trigger. Choose a stable result container and
decide whether focus should remain on the trigger or move to the new result.
Working example
<section ng-init="requestStatus = 'Results not loaded'">
<button
ng-get="/account/orders"
data-target="#order-results"
swap="innerHTML"
loading
on-success="requestStatus = 'Orders loaded'; orderResults.focus()"
on-error="requestStatus = $res.message"
aria-describedby="order-status"
>
Load orders
</button>
<p id="order-status" role="status" aria-live="polite">
{{ requestStatus }}
</p>
<div id="order-results" ng-el="orderResults" tabindex="-1"></div>
</section>
Server contract
Return a heading and order rows as one HTML fragment. Return JSON with a short,
actionable message for errors so the live region can announce it.
What AngularTS wires
The native button keeps keyboard activation. loading exposes request state,
role="status" announces text changes, and ng-el provides the stable result
element without a selector search.
Failure path
Do not move focus on failure or for background refreshes. Never replace the
focused trigger with a swap unless the response provides the next focus target.
Apply it now
Run one server-driven interaction using only a keyboard and a screen reader.
Name the trigger, loading announcement, error announcement, and final focus
location before changing markup.
Verify
Activate with Enter and Space, delay the response, reject it, and return an empty
result. Confirm status is announced once, focus is visible, and tab order remains
logical.
31 - Audit a server-driven interaction before shipping
Check authorization, CSRF, HTML trust, caching, uploads, and errors
Problem
A form or HTTP directive works, but its security depends on decisions spread
between markup, request configuration, the endpoint, and storage.
Before you start
Choose one real state-changing request. Record its authenticated user, resource,
method, credential transport, response type, cache path, and uploaded content.
Audit the interaction
| Surface | Shipping check |
|---|
| Authorization | Server verifies this user may act on this resource |
| Cookie request | Server validates CSRF protection on every state-changing method |
| Bearer credential | Token never appears in URL, HTML, storage logs, or error output |
| HTML response | Server escapes untrusted values before AngularTS compiles it |
| JSON error | Body contains safe field messages, not stack or database details |
| Cache | Private entries are partitioned and cleared on logout |
| Upload | Server checks size, MIME signature, filename, and storage limits |
| Redirect | Destination is allowlisted rather than copied from user input |
Client validation, hidden buttons, route policy, and disabled controls improve
the user experience. None of them authorize a server operation.
Failure path
Use 401 when authentication is missing, 403 when the authenticated user lacks
permission, and 422 for correctable input. Do not turn a security rejection
into a successful response just to simplify the template.
Apply it now
Capture one real request in the Network panel and answer every row using the
server implementation, not assumptions from the UI. Add a server test for each
answer that can regress.
Verify
Repeat the request signed out, as another user, without the CSRF proof, with
tampered IDs, with malicious text, and with an oversized upload when applicable.
Confirm every rejection is safe and leaves no unauthorized state change.
32 - Keep a form useful before AngularTS starts
Build a normal server form first, then add faster in-page behavior
Problem
A request should still work when JavaScript is slow, unavailable, or fails to
start.
Before you start
Make the endpoint accept a normal form submission and return a complete page.
Only then add AngularTS behavior to improve the interaction.
<form
action="/orders"
method="post"
ng-post="/orders"
data-state-success="orders"
on-error="errors = $res"
>
<label>
Quantity
<input name="quantity" type="number" min="1" required />
</label>
<button type="submit">Place order</button>
<p ng-if="errors.quantity">{{ errors.quantity }}</p>
</form>
Without AngularTS, the browser posts the form to /orders. With AngularTS, the
HTTP directive submits it without a page reload and the router enters orders
after success. The endpoint must accept both the browser’s form encoding and the
enhanced request body.
Server contract
For a normal browser submission, return a complete success or validation page.
For the enhanced request, return 2xx on success or a JSON error object such as
{"quantity":"Choose at least one item"} with 422. Decide how the server
distinguishes these requests; do not depend on an undocumented client header.
Failure path
Keep native labels, required fields, methods, actions, and submit buttons. A
network failure may prevent either form of submission, but it must not leave a
control that only works after JavaScript wiring.
Apply it now
Choose one important form. Remove JavaScript mentally and check whether its
action, method, controls, and server response still complete the task.
Verify
Submit once with JavaScript disabled and once with AngularTS running. Test valid
input, invalid input, a signed-out session, and a server error in both paths.
33 - Debug an HTTP interaction from the browser
Trace an element, its scope, its request, and the resulting UI state
Problem
A click or submission produces the wrong result and you need to find whether the
problem is the element, scope, request, response, or rendering.
Before you start
Open the Elements, Console, and Network panels. Select the element that owns the
HTTP directive so it is available as $0 in the console.
Follow one interaction
const scope = angular.getScope($0);
scope.order;
Check the directive attributes on $0, inspect the values used by its
expressions, then perform the interaction with Network recording enabled. Read
the request method, URL, payload, status, content type, and response before
looking at the rendered result.
After the response, inspect the scope again. If the response is correct but the
scope is not, check the directive assignment such as $res, on-success, or
on-error. If scope is correct but the page is not, inspect the binding or
conditional directive that renders it.
Failure path
Do not debug only the successful request. Preserve the failed response in the
Network panel and inspect the error branch, including status-specific behavior
for 401, 403, 409, 422, and 5xx responses.
Apply it now
Take one interaction that currently feels opaque and write down the value at
each step: element, scope before request, request, response, scope after request,
and DOM result. The first unexpected value identifies the layer to fix.
Verify
Repeat the trace after the fix. Confirm the request occurs once, the correct
branch runs, and a failed response leaves the scope and DOM in a usable state.
34 - Control repeated requests
Measure duplicate work, then delay, throttle, cache, or cancel it
Problem
Typing, scrolling, hovering, or repeated clicks can start more requests than the
user needs.
Before you start
Record the interaction in the Network panel. Count requests, transferred bytes,
server time, and stale responses. Do not add request control without seeing the
problem first.
Choose the smallest control
| What you observe | First response |
|---|
| Fast typing starts a request per key | Add a short delay after input settles |
| Scroll or resize fires continuously | Throttle the event |
| The same read repeats unchanged | Reuse an appropriate public or user-partitioned cache |
| An old search overwrites a newer one | Reject stale results or cancel the old request |
| Hover prefetch repeats | Guard it with loaded and loading state |
| Double-click repeats a write | Disable while pending and make the server idempotent |
Use a promise-valued $http timeout to cancel an obsolete programmatic
request. Treat xhrStatus === "abort" as cancellation rather than a server
failure. HTTP directive flows should usually prevent another trigger while the
current request is pending instead of creating custom cancellation code.
Server contract
Search and list endpoints must tolerate reordered reads. Write endpoints must
protect against duplicate effects with an idempotency key, version, or another
server-side rule when repetition could charge, send, or mutate twice.
Failure path
Always clear loading state after success, error, or cancellation. Never cache an
authorization failure or leak user-specific data through a shared cache.
Apply it now
Find the noisiest interaction in the Network panel. Apply only the first matching
row, then measure it again before combining techniques.
Verify
Trigger the interaction rapidly, use a slow network profile, and force responses
to arrive out of order. Confirm the newest intent wins and writes happen once.
35 - Show validation errors returned by the server
Keep validation authoritative on the server and render field errors in place
Problem
A form needs immediate, useful errors without duplicating the server’s validation
rules in browser code.
Before you start
Make the endpoint return 422 with a JSON object whose keys match form control
names. Use short messages that are safe to show directly to the user.
Render the error object
<form
ng-post="/account/profile"
on-success="profile = $res; errors = {}"
on-error="errors = $res"
>
<label ng-class="{ error: errors.email }">
Email
<input
name="email"
type="email"
value="{{ profile.email }}"
aria-invalid="{{ errors.email !== undefined }}"
ng-keyup="errors.email = undefined"
/>
<span ng-if="errors.email">{{ errors.email }}</span>
</label>
<button type="submit">Save profile</button>
</form>
The same error object drives the message, error class, and accessibility state.
Browser validation can improve feedback, but the server still checks every value.
Server contract
Return the saved profile with 2xx. Return a body such as
{"email":"Use a work email address"} with 422 for correctable input.
Reserve 401, 403, 409, and 5xx for their distinct meanings.
Failure path
If an error is not a field error, show a form-level message instead of assigning
it to an unrelated control. Never return stack traces or database messages.
Apply it now
Pick one form that reimplements server validation in JavaScript. Make the server
return named errors and bind those errors beside the matching controls.
Verify
Submit empty, malformed, duplicate, unauthorized, and valid values. Confirm the
right field is identified and keyboard focus remains usable after every response.
36 - Protect a cookie-authenticated form from CSRF
Send a server-issued proof with every state-changing form request
Problem
A form uses authentication cookies, so another site may be able to cause the
browser to send an unwanted state-changing request.
Before you start
Configure CSRF protection on the server. Generate a token tied to the user or
session and render it into the page. Cookie SameSite settings help, but they do
not replace server-side CSRF validation.
Submit the server-issued token
<form ng-post="/account/email" on-error="errors = $res">
<input
type="hidden"
name="csrf_token"
value="SERVER_RENDERED_TOKEN"
/>
<label>
New email
<input name="email" type="email" required />
</label>
<button type="submit">Change email</button>
</form>
Replace SERVER_RENDERED_TOKEN in the server template. AngularTS serializes the
named hidden control with the rest of the form; the server validates it before
changing state.
Server contract
Reject a missing, expired, or mismatched token without performing the operation.
Rotate tokens according to the server framework’s policy and compare them using
its maintained CSRF implementation rather than custom cryptography.
Failure path
Return a safe 403 response when the proof fails. Do not automatically retry the
write because a fresh token may require a new authenticated page response.
Apply it now
Inspect every cookie-authenticated POST, PUT, PATCH, and DELETE endpoint.
Start with account, payment, and permission changes.
Verify
Send the request with the valid token, no token, a token from another session,
and an expired token. Confirm only the valid request changes state.
37 - Confirm a destructive action in place
Require an explicit second action before sending a delete request
Problem
A single accidental click can delete important data.
Before you start
Make deletion authorization and resource ownership checks authoritative on the
server. Decide whether the operation is reversible and say so in the interface.
Ask for a second explicit action
<article ng-init="confirming = false; deleted = false" ng-if="!deleted">
<h2>Quarterly report</h2>
<button type="button" ng-click="confirming = true">Delete</button>
<div ng-if="confirming" role="alert">
<p>This permanently deletes the report.</p>
<button
type="button"
ng-delete="/api/reports/42"
on-success="deleted = true"
on-error="deleteError = $res"
>
Delete permanently
</button>
<button type="button" ng-click="confirming = false">Cancel</button>
<p ng-if="deleteError">{{ deleteError.message }}</p>
</div>
</article>
The first button changes only local state. The second button sends the request
and names the exact consequence.
Server contract
Return 204 after deletion. Return 404 when the resource no longer exists and
403 when the user may not delete it. Make repeated deletion safe when possible.
Failure path
Keep the confirmation visible after failure so the user can read the message or
cancel. Do not remove the item until the server confirms deletion.
Apply it now
Find the most damaging one-click action in the application and split intent from
execution. Use a specific label such as “Delete report,” not “Yes.”
Verify
Test keyboard-only confirmation, cancellation, double activation, a missing
resource, and a permission rejection. Confirm only one authorized delete occurs.
38 - Handle two people editing the same record
Detect stale updates and let the user compare the current server value
Problem
One person can overwrite a change saved by someone else while both are editing
the same record.
Before you start
Give each record a version, revision, or strong ETag. Require the client to send
the version it originally loaded with every update.
Send the version with the edit
<form
ng-put="/api/articles/42"
on-success="article = $res; conflict = undefined"
on-error="conflict = $res"
>
<input name="version" type="hidden" value="{{ article.version }}" />
<label>
Title
<input name="title" value="{{ article.title }}" required />
</label>
<button type="submit">Save article</button>
<aside ng-if="conflict.current">
<p>Someone saved a newer version: {{ conflict.current.title }}</p>
<button
type="button"
ng-click="article = conflict.current; conflict = undefined"
>
Load newer version
</button>
</aside>
</form>
Server contract
Update only when the submitted version matches the stored version. Otherwise
return 409 with a safe representation of the current record. Increment the
version atomically with the successful update.
Failure path
Do not silently retry a conflicting write. Preserve the user’s draft while
showing the current server value so they can compare or merge intentionally.
Apply it now
Add a version check to one record that is commonly edited by multiple people.
Start with content, inventory, scheduling, or permission records.
Verify
Open the same record in two sessions. Save different values in both and confirm
the second save receives 409 without overwriting the first.
39 - Let the user retry a failed read
Keep the last useful state and retry only when the user asks
Problem
A temporary network or server failure leaves a section empty or forces a full
page reload.
Before you start
Keep reads safe and idempotent. Decide whether old data is more useful than an
empty screen while a refresh fails.
Keep recovery next to the result
<section ng-init="orders = []; loadError = undefined">
<button
type="button"
ng-get="/api/orders"
on-success="orders = $res; loadError = undefined"
on-error="loadError = $res"
>
Refresh orders
</button>
<p ng-if="loadError" role="alert">
Orders could not be refreshed. Try again.
</p>
<p ng-if="!loadError && orders.length === 0">No orders yet.</p>
<ul>
<li ng-repeat="order in orders">{{ order.number }}</li>
</ul>
</section>
The existing list remains visible if refresh fails. The user controls when
another request is sent.
Server contract
Return a JSON array with 2xx. Use 401 for an expired session and 5xx for a
temporary server failure. Include a safe request ID in unexpected errors.
Failure path
Do not retry writes automatically. Avoid unlimited automatic read retries; they
can amplify an outage and consume mobile data while the user cannot intervene.
Apply it now
Choose one useful read that currently disappears on failure. Preserve its last
successful value and put a retry control beside the error.
Verify
Load successfully, then force offline mode and retry. Confirm the old result
stays visible, the error is announced, and each click sends exactly one request.
40 - Paginate a large result on the server
Keep URLs shareable while replacing only the result region
Problem
A large collection is slow to query, transfer, render, and navigate.
Before you start
Make the server return a bounded page and stable next and previous URLs. Prefer a
cursor when records change frequently; use page numbers when random access is
important and the data is stable enough.
Keep real navigation URLs
<section>
<div id="orders">
<p>Orders 21–40</p>
</div>
<nav aria-label="Order pages">
<a
href="/orders?page=1"
ng-get="/orders?page=1"
data-target="#orders"
swap="innerHTML"
>
Previous
</a>
<a
href="/orders?page=3"
ng-get="/orders?page=3"
data-target="#orders"
swap="innerHTML"
>
Next
</a>
</nav>
</section>
The href remains useful for opening a page in another tab and for normal
navigation before AngularTS starts. The HTTP directive can replace only the list
when the enhanced interaction runs.
Server contract
Return a complete page for normal navigation and a trusted list fragment for the
enhanced request according to the application’s explicit response policy. Keep
sort and filter values in every pagination URL.
Failure path
Leave the current page visible if the next page fails. Do not advance the visible
page label or browser URL until the requested result succeeds.
Apply it now
Measure the largest rendered list. Move its limit, ordering, and cursor to the
server before adding virtual scrolling or client-side collection machinery.
Verify
Open next and previous links normally, in a new tab, with JavaScript disabled,
and with AngularTS running. Confirm the same records and ordering appear.
41 - Download a generated file with the browser
Use normal navigation for files instead of loading bytes into application state
Problem
A report or export needs to download without turning a large binary response into
scope state or manually constructing a browser blob.
Before you start
Make the server stream the file with the correct Content-Type and a safe
Content-Disposition filename. Authorize the download on every request.
Let the browser own the response
<form action="/reports/orders.csv" method="get">
<label>
From
<input name="from" type="date" required />
</label>
<label>
To
<input name="to" type="date" required />
</label>
<button type="submit">Download CSV</button>
</form>
A native form lets the browser stream, name, save, and expose download progress
without retaining the file in AngularTS memory.
Server contract
Validate dates, cap the export size, escape spreadsheet-formula prefixes when
creating CSV, and stream rows instead of building the entire file in memory.
Failure path
For long exports, create a background job and return a status page rather than
holding an HTTP request indefinitely. Show authorization or validation errors as
a normal server page.
Apply it now
Find one download implemented with $http, base64, or a manually created blob.
Replace it with a normal authorized download when no client transformation is
required.
Verify
Test small, empty, large, unauthorized, and maliciously named exports. Confirm
memory stays stable and the downloaded filename and content type are correct.
42 - Replace a region with trusted server HTML
Keep rendering on the server without compiling untrusted markup
Problem
The server already renders a useful partial and returning JSON would duplicate
the same presentation rules in a client template.
Before you start
Choose a stable target element. Treat every returned fragment as executable UI:
escape untrusted values on the server and allow only application-owned markup.
Swap the fragment into a named region
<section>
<button
type="button"
ng-get="/account/summary"
data-target="#account-summary"
swap="innerHTML"
>
Refresh summary
</button>
<div id="account-summary" aria-live="polite">
<p>Summary not loaded.</p>
</div>
</section>
AngularTS wires directives and bindings in the returned application-owned
fragment. That makes server escaping and content ownership security requirements,
not optional cleanup.
Server contract
Return text/html containing valid fragment markup. Keep scripts, inline event
handlers, user-authored templates, and unsanitized rich text out of the response.
Failure path
Keep the old region on failure and show an error beside the refresh control.
Never swap an authentication page or generic proxy error into the target.
Apply it now
Choose one JSON endpoint whose client template simply recreates existing server
HTML. Return the existing partial directly and remove the duplicate renderer.
Verify
Test names containing <, >, quotes, and markup-like text. Confirm they render
as text, the fragment has balanced tags, and its controls work after replacement.
43 - Show a request ID when an operation fails
Give users and operators one safe identifier for tracing a failure
Problem
A user reports “Save failed,” but operators cannot connect that report to one
server request among thousands.
Before you start
Generate or accept a request ID at the trusted server edge, carry it through
logs and downstream calls, and return it in safe unexpected-error responses.
Render the identifier with the error
<form ng-post="/api/invoices" on-error="saveError = $res">
<label>
Customer
<input name="customer" required />
</label>
<button type="submit">Create invoice</button>
<div ng-if="saveError" role="alert">
<p>The invoice could not be created.</p>
<p ng-if="saveError.requestId">
Support code: {{ saveError.requestId }}
</p>
</div>
</form>
The user sees a stable support code, not internal diagnostics.
Server contract
Return a body such as {"message":"Request failed","requestId":"req_..."} for
unexpected errors. Log the same ID with route, status, duration, and safe context.
Never put credentials, personal data, stack traces, or SQL in the response.
Failure path
If the client supplies an ID, validate its format before logging it. Keep an
independently generated internal trace identifier when crossing trust boundaries.
Apply it now
Take one high-value write and follow its request ID from the response to every
relevant server log. Remove any step that requires searching by timestamp alone.
Verify
Force a controlled 5xx, copy the displayed code, and find exactly one request
path in logs. Confirm the response exposes no implementation details.
44 - Check a server interaction before shipping
Walk one feature through behavior, failure, access, and operations
Problem
An interaction works in the happy path but its production behavior has not been
checked as one complete system.
Before you start
Choose one real user action and use its production-like endpoint and permissions.
Open the Elements, Console, and Network panels.
Walk the whole path
Check these in order:
| Step | Question |
|---|
| Page | Does the useful server-rendered page work before enhancement? |
| Input | Are labels, names, constraints, and server validation aligned? |
| Request | Is the method correct and sent only once? |
| Access | Does the server check authentication, authorization, and CSRF? |
| Response | Are status, content type, and body appropriate? |
| State | Does success update only the intended scope and DOM? |
| Failure | Can the user understand, retry, cancel, or recover safely? |
| Concurrency | Can stale or duplicate requests corrupt state? |
| Accessibility | Are status and focus understandable without a pointer? |
| Operations | Can a request ID connect the visible failure to server logs? |
Failure path
Treat any unanswered row as unfinished implementation rather than documentation
debt. Fix the smallest owning layer: browser markup, AngularTS wiring, endpoint,
storage transaction, or operational logging.
Apply it now
Run the table against the newest state-changing interaction. Record evidence for
each answer: a browser trace, server test, accessibility check, or log lookup.
Verify
Repeat with invalid input, no permission, slow networking, duplicate activation,
stale data, and a controlled server failure. Ship when every outcome is deliberate.
45 - Recover from a rate-limited request
Respect 429 responses without hiding retries or multiplying traffic
Problem
An endpoint limits repeated requests, but the page reports a generic failure or
immediately retries and makes the limit worse.
Before you start
Make the server return 429 and a Retry-After header. Use rate limits to
protect a resource, not as the only defense against abusive or unauthorized use.
Keep retry under the user’s control
<section ng-init="limitError = undefined">
<button
type="button"
ng-get="/api/reports/preview"
on-success="preview = $res; limitError = undefined"
on-error="limitError = $res"
>
Refresh preview
</button>
<p ng-if="limitError" role="alert">
{{ limitError.message }} Try again after {{ limitError.retryAfter }}.
</p>
<div ng-if="preview">{{ preview.summary }}</div>
</section>
Do not run a tight automatic retry loop. Keep the last useful result visible and
tell the user when another attempt can succeed.
Server contract
Return 429, Retry-After, and a safe body such as
{"message":"Preview limit reached","retryAfter":"14:30"}. Apply the limit to
the correct user, tenant, token, or resource rather than only an IP address.
Failure path
Treat 429 differently from authentication, permission, validation, and server
failures. Do not cache the response longer than the stated limit window.
Apply it now
Find the endpoint most likely to receive bursts. Confirm the page exposes the
limit instead of silently creating more traffic.
Verify
Exceed the limit, inspect the header and body, wait for the window, and retry.
Confirm no background loop continues while the request is limited.
46 - Run slow work as a background job
Start work quickly, expose status, and download the finished result
Problem
A report, import, or media conversion takes too long for one request and becomes
fragile behind browser, proxy, or deployment timeouts.
Before you start
Give the job a durable ID, owner, state, progress value, and expiry policy. Make
starting the same job twice safe when duplicate work would be expensive.
Start the job, then ask for status
<section>
<form ng-post="/api/exports" on-success="job = $res">
<label>
Format
<select name="format"><option value="csv">CSV</option></select>
</label>
<button type="submit">Create export</button>
</form>
<div ng-if="job">
<p>Status: {{ job.status }}</p>
<button
type="button"
ng-get="/api/exports/current"
on-success="job = $res"
>
Refresh status
</button>
<a ng-if="job.downloadUrl" href="{{ job.downloadUrl }}">Download</a>
</div>
</section>
Use manual refresh first. Add measured polling or server-sent events only when
the user benefits from continuous progress.
Server contract
Return 202 with the accepted job representation. The status endpoint returns
queued, running, completed, failed, or cancelled. Authorize status and download
URLs against the job owner on every request.
Failure path
Keep a failed job record with a safe explanation and request ID. Do not expose
worker stack traces or leave failed uploads and generated files indefinitely.
Apply it now
Find the longest request in production traces. Move it to a job when its work can
continue independently of the browser connection.
Verify
Test duplicate starts, worker restart, cancellation, failure, expiry, another
user’s job ID, and download after completion.
47 - Make a critical write safe to repeat
Prevent double charges, sends, and submissions with an idempotency key
Problem
A double click, reconnect, proxy retry, or impatient refresh can submit the same
critical operation more than once.
Before you start
Generate a unique operation key on the server-rendered page. Scope it to the
authenticated user and operation, and store the first result atomically.
<form ng-post="/api/payments" on-error="paymentError = $res">
<input
type="hidden"
name="idempotency_key"
value="SERVER_RENDERED_OPERATION_KEY"
/>
<label>
Amount
<input name="amount" inputmode="decimal" required />
</label>
<button type="submit">Pay once</button>
<p ng-if="paymentError" role="alert">{{ paymentError.message }}</p>
</form>
Disabling the button improves feedback but cannot provide this guarantee. The
server key protects retries from every source.
Server contract
For the same user, operation, key, and payload, return the stored first result.
Reject reuse of the key with a different payload. Expire records only after the
operation can no longer be retried safely.
Failure path
Do not mint a new key merely because the response was lost. Reuse the operation
key until the server confirms whether the first request completed.
Apply it now
Add idempotency to the operation with the highest cost of duplication: payment,
invitation, email, provisioning, or irreversible workflow transition.
Verify
Send identical requests concurrently, disconnect after sending, and retry with
the same key. Confirm one effect and one stable result are recorded.
48 - Build a multi-step form on the server
Save each step independently instead of keeping a fragile browser-only draft
Problem
A long form needs several steps, but refreshing, navigating back, or changing
devices must not discard completed work.
Before you start
Create a server-side draft with an owner and current step. Validate and save each
step independently. Make every step addressable with a normal URL.
<form
action="/checkout/address"
method="post"
ng-post="/checkout/address"
data-state-success="checkout-payment"
on-error="errors = $res"
>
<label>
Delivery address
<textarea name="address" required></textarea>
</label>
<p ng-if="errors.address">{{ errors.address }}</p>
<button type="submit">Continue to payment</button>
</form>
The server saves the address before advancing. AngularTS can enter the next
router state after success, while the native action remains a working fallback.
Server contract
Return 422 for step errors, redirect a normal successful submission to the next
URL, and return 2xx for the enhanced request. Reject steps the user is not yet
allowed to complete.
Failure path
Keep the saved draft when a later step fails. Never trust a hidden step number as
proof that earlier requirements or payment checks succeeded.
Apply it now
Choose the longest form and split it at points where the server can save a useful
draft. Keep related fields together rather than creating arbitrary page counts.
Verify
Refresh every step, use the Back button, open the draft on another device, submit
steps out of order, and resume after a failed request.
49 - Apply one action to several records
Submit explicit selections and return a result for every record
Problem
Users need to update several records without opening and submitting each one
individually.
Before you start
Choose a maximum batch size. Authorize every selected record independently and
decide whether the operation is atomic or may partially succeed.
Submit explicit selected controls
<form ng-post="/api/tasks/archive" on-success="result = $res">
<fieldset>
<legend>Tasks to archive</legend>
<label><input name="task_42" type="checkbox" /> Draft invoice</label>
<label><input name="task_43" type="checkbox" /> Review contract</label>
</fieldset>
<button type="submit">Archive selected tasks</button>
<p ng-if="result">Archived {{ result.archived }} tasks.</p>
<ul><li ng-repeat="error in result.errors">{{ error.message }}</li></ul>
</form>
Named controls make the submitted selection inspectable in the Network panel.
For dynamic lists, use the server’s documented array representation consistently.
Server contract
Return a count and per-record failures. If partial success is allowed, make the
response identify exactly which records changed. If it must be atomic, perform
all authorization and validation before committing any record.
Failure path
Do not report the whole batch as successful when some records failed. Preserve
failed selections so the user can correct or retry them.
Apply it now
Find a repetitive list action and define its batch limit, authorization rule,
transaction behavior, and response before adding selection controls.
Verify
Test no selection, the maximum size, duplicates, mixed permissions, missing
records, partial failure, and a repeated submission.
50 - Archive first and offer restore
Make a destructive-looking action recoverable when the domain allows it
Problem
Users need to remove an item from active work, but permanent deletion is too easy
to regret.
Before you start
Model archived state on the server. Define retention, visibility, uniqueness,
restore permissions, and the separate conditions for permanent deletion.
Keep restore beside the result
<section ng-init="archived = false">
<button
ng-if="!archived"
type="button"
ng-post="/api/projects/42/archive"
on-success="archived = true"
>
Archive project
</button>
<div ng-if="archived" role="status">
<p>Project archived.</p>
<button
type="button"
ng-post="/api/projects/42/restore"
on-success="archived = false"
>
Restore
</button>
</div>
</section>
The page changes only after each server operation succeeds.
Server contract
Make archive and restore idempotent. Record who changed the state and when. Keep
permanent deletion as a separate, more restricted operation.
Failure path
If restore fails, keep the archived message and explain the conflict or permission
problem. Do not imply that a client-side undo can reverse a committed server action.
Apply it now
Replace one permanent delete with archive and restore where legal and operational
requirements allow retention.
Verify
Archive twice, restore twice, restore after a naming conflict, test another user,
and confirm archived records stay out of active queries.
51 - Handle a permission rejection without hiding it
Let the server decide access and give the user a useful next step
Problem
The interface hides controls based on known permissions, but permissions can
change and direct or stale requests still reach the server.
Before you start
Authorize every operation on the server using the authenticated user and current
resource. Client visibility is presentation, not enforcement.
Render the server rejection
<form
ng-put="/api/team/42/settings"
on-success="settings = $res; accessError = undefined"
on-error="accessError = $res"
>
<label>
Team name
<input name="name" value="{{ settings.name }}" required />
</label>
<button type="submit">Save settings</button>
<p ng-if="accessError" role="alert">
{{ accessError.message }}
</p>
</form>
A disabled or hidden button can reduce confusion when permission is already
known, but the rejection path remains necessary.
Server contract
Return 401 when authentication is missing and 403 when the authenticated user
lacks permission. Keep the message safe when even resource existence is private.
Failure path
Do not redirect every 403 to login; signing in again cannot grant a missing
permission. Offer a useful next step such as returning, requesting access, or
contacting an administrator.
Apply it now
Open one privileged action in two sessions, remove permission in one, and submit
the stale form in the other. Make the resulting rejection understandable.
Verify
Test signed out, wrong role, wrong tenant, removed permission, deleted resource,
and authorized access. Confirm no rejected request changes state.
52 - Keep search and filters in the URL
Make filtered results reloadable, shareable, and usable without JavaScript
Problem
Filters exist only in browser state, so refresh, sharing, Back, and opening a new
tab lose the current result.
Before you start
Define stable query parameter names and defaults on the server. Return the same
result for the same authorized URL.
<form
action="/orders"
method="get"
ng-get="/orders"
data-target="#orders"
swap="innerHTML"
>
<label>Status <select name="status"><option>Open</option></select></label>
<label>Search <input name="q" type="search" /></label>
<button type="submit">Apply filters</button>
</form>
<section id="orders" aria-live="polite">
<p>Choose filters to update the result.</p>
</section>
The native GET form defines the canonical URL. Enhancement may replace the result
region, but it must preserve a URL that reproduces the same filters.
Server contract
Parse, validate, normalize, and cap query values. Include normalized filters in
pagination links and render them back into the controls on complete pages.
Failure path
Leave the current result visible when an enhanced request fails. Do not write an
unconfirmed filter state into browser history.
Apply it now
Refresh the most-used filtered list. Move every lost filter into a named GET
control and make the resulting URL reproduce the page.
Verify
Submit normally, copy the URL, open it in another tab, use Back and Forward, and
repeat with JavaScript disabled and AngularTS running.
53 - Log out with a state-changing form
End the server session, clear private state, and avoid logout links
Problem
Logout is implemented as a GET link or only clears browser state, leaving the
server session active or allowing unwanted cross-site logout requests.
Before you start
Provide a server POST endpoint that invalidates the session and rotates relevant
credentials. Decide which private caches and client state must be discarded.
<form
action="/logout"
method="post"
ng-post="/logout"
data-state-success="signed-out"
>
<input
type="hidden"
name="csrf_token"
value="SERVER_RENDERED_TOKEN"
/>
<button type="submit">Log out</button>
</form>
The normal form works without client routing. The enhanced form can enter a
public signed-out state after the server confirms invalidation.
Server contract
Invalidate the session, clear or expire authentication cookies, and return a
public response. Make repeated logout safe. Never use GET for this state change.
Failure path
If the server cannot confirm logout, do not claim the session ended. Avoid keeping
private response data in a cache that another signed-in user can inherit.
Apply it now
Inspect the current logout request and subsequent Back navigation. Remove any
private page or cached data that remains usable after a confirmed logout.
Verify
Log out normally and with AngularTS, repeat the request, use Back, open a private
URL, and sign in as another user in the same browser.
54 - Keep requests same-origin unless they must cross origins
Configure CORS and credentials deliberately for a separate API origin
Problem
The page and API use different origins, introducing preflight,
credential, cookie, caching, and deployment behavior that same-origin requests
avoid.
Before you start
Ask whether the application server can proxy the API under the page’s origin.
Prefer that simpler setup unless direct browser access to the other origin is a
real requirement.
Allow only known application origins, required methods, and required headers.
When cookies are involved, use explicit origins and credentials; wildcard origin
responses cannot safely represent authenticated access. Keep CSRF protection for
cookie-authenticated state changes.
Server contract
Handle preflight requests consistently, vary cached responses by origin when
needed, and return the same authorization and validation statuses as a same-origin
endpoint. Treat CORS as browser access policy, not user authorization.
Failure path
Do not disable browser security, reflect arbitrary Origin values, put secrets
in URLs, or convert a CORS failure into a JSONP-style executable response.
Apply it now
List every production origin that can call the API and every credential it sends.
Remove unused origins, headers, and methods from the server policy.
Verify
Test the allowed production origin, an unlisted origin, preflight, credentials,
cache behavior, and a state-changing request without CSRF proof.
55 - Handle a uniqueness race on submit
Let the database decide uniqueness and return the conflict to the field
Problem
The page checks that a name is available, but another request claims it before
the user submits.
Before you start
Enforce uniqueness with a database constraint. Availability checks improve
feedback but can never reserve the value unless the server explicitly does so.
Return the conflict to the field
<form ng-post="/api/teams" on-error="errors = $res">
<label ng-class="{ error: errors.slug }">
Team address
<input
name="slug"
required
aria-invalid="{{ errors.slug !== undefined }}"
ng-keyup="errors.slug = undefined"
/>
<span ng-if="errors.slug">{{ errors.slug }}</span>
</label>
<button type="submit">Create team</button>
</form>
Server contract
Attempt the insert and translate the known uniqueness violation into 409 or a
field-oriented 422 body such as {"slug":"That address is already used"}.
Do not expose database error text.
Failure path
Preserve every other form value. Ask the user to choose another value instead of
retrying the same conflicting write automatically.
Apply it now
Find one pre-submit uniqueness check and confirm the final write still handles a
database constraint violation as an expected outcome.
Verify
Submit the same value concurrently from two sessions. Confirm one succeeds, one
gets the field error, and no duplicate record is stored.
56 - Submit money without floating-point surprises
Send a decimal string and perform currency arithmetic on the server
Problem
An amount changes by a cent because browser or server code treats currency as a
binary floating-point number.
Before you start
Choose the currency, accepted decimal precision, rounding rule, minimum, and
maximum on the server. Never infer currency only from the user’s locale.
Submit the entered decimal text
<form ng-post="/api/invoices" on-error="errors = $res">
<label>
Amount in EUR
<input
name="amount"
inputmode="decimal"
autocomplete="off"
required
/>
</label>
<input name="currency" type="hidden" value="EUR" />
<p ng-if="errors.amount">{{ errors.amount }}</p>
<button type="submit">Create invoice</button>
</form>
Server contract
Parse the amount with a decimal or money type, validate its scale, and store
minor units or an exact decimal together with the currency. Return formatted
display text separately from the canonical value when needed.
Failure path
Reject ambiguous separators and unsupported precision with 422. Do not silently
round an amount when that changes what the user authorized.
Apply it now
Trace one amount from input through JSON, application code, database, and output.
Remove every binary floating-point conversion from that path.
Verify
Test 0.01, large values, too many decimal places, negative values, comma input,
and a currency with a different minor-unit rule.
57 - Submit dates and times with an explicit time zone
Distinguish calendar dates, local times, and exact instants
Problem
A date or appointment moves by a day or hour when users and servers use different
time zones.
Before you start
Decide whether each value is a calendar date, a local wall-clock time, or an exact
instant. These are different domain values and need different storage rules.
Send the local value and zone together
<form ng-post="/api/appointments" on-error="errors = $res">
<label>
Local start
<input name="localStart" type="datetime-local" required />
</label>
<label>
Time zone
<select name="timeZone" required>
<option value="Europe/Riga">Europe/Riga</option>
</select>
</label>
<p ng-if="errors.localStart">{{ errors.localStart }}</p>
<button type="submit">Schedule</button>
</form>
Server contract
Resolve local time with an IANA zone, reject nonexistent daylight-saving times,
and define how ambiguous times are chosen. Store exact instants in UTC while
retaining the zone when future local scheduling depends on it.
Failure path
Do not append Z to a local value or assume the server’s zone. Return 422 when
the local time cannot identify the intended instant.
Apply it now
Classify every date field in one workflow as date, local time, or instant. Rename
ambiguous fields and include the missing zone where required.
Verify
Test users in two zones, both daylight-saving transitions, leap day, midnight,
and a date-only value that must never shift.
58 - Keep identifiers as opaque strings
Avoid rounding, parsing, and leaking meaning through record IDs
Problem
A large numeric ID is rounded in JavaScript or application code starts depending
on structure that should be private to the server.
Before you start
Define public identifiers as strings even when the database uses numbers. Treat
them as labels, not values for arithmetic or ordering.
Submit the identifier unchanged
<form ng-post="/api/invitations" on-error="errors = $res">
<input
name="accountId"
type="hidden"
value="acct_9007199254740993"
/>
<label>Email <input name="email" type="email" required /></label>
<button type="submit">Invite</button>
</form>
Server contract
Accept and return the identifier as a string. Validate its syntax, then authorize
the referenced resource independently. An unguessable ID is not permission.
Failure path
Reject malformed IDs without coercing them. Avoid messages that reveal private
resource existence when the requester lacks access.
Apply it now
Search one API response for identifier fields typed as numbers. Convert public
IDs to strings before they cross the server boundary.
Verify
Round-trip IDs larger than JavaScript’s safe integer, leading-zero IDs, UUIDs,
and malformed strings without changing their text.
59 - Revalidate a cached response with ETag
Return 304 when unchanged instead of transferring the same representation
Problem
The browser repeatedly downloads a response that rarely changes.
Before you start
Confirm the request is safe and cacheable. Decide whether the representation is
public, user-specific, or private before choosing cache headers.
Return a stable strong or weak ETag with the representation. On a later request,
compare If-None-Match and return 304 without a body when it still matches.
Normal navigation and standards-based HTTP clients can then reuse cached bytes.
Server contract
Change the ETag whenever the selected representation changes. Send appropriate
Cache-Control and Vary headers. Partition authenticated content and never mark
private user data as shared public cache content.
Failure path
Do not use 304 when authorization changed or when the selected representation
differs by language, encoding, origin, or user and the cache key ignores it.
Apply it now
Use the Network panel to find the largest repeated unchanged read. Add validators
at its server renderer before introducing a custom application cache.
Verify
Request once, revalidate, change the record, and revalidate again. Confirm 304
only while the body is unchanged and no private response crosses users.
60 - Keep a useful page during planned maintenance
Return a clear 503 response while preserving public navigation and recovery
Problem
A deployment or dependency outage turns the application into an indefinite
spinner, proxy error, or misleading authentication failure.
Before you start
Prepare a small server-rendered maintenance page that does not depend on the
database or application service being maintained.
Return the correct temporary status
Use 503 Service Unavailable and a realistic Retry-After header. Explain what
is unavailable, whether existing data is safe, and when the user should retry.
Keep status and support links on dependencies that remain available.
Server contract
Health and load-balancer behavior must remove unhealthy instances without caching
the maintenance page as a permanent success. Static assets required by the page
must remain reachable or be embedded conservatively.
Failure path
Do not return 200 for a failed application page, redirect repeatedly, or expose
deployment details. Avoid automatic browser retries that create a recovery surge.
Apply it now
Open the application with its database unavailable in a production-like setup.
Replace the first raw failure with the smallest dependable maintenance response.
Verify
Test planned maintenance, an unexpected dependency outage, crawler behavior,
cache headers, recovery, and multiple browser tabs retrying together.
61 - Return users safely after sign-in
Preserve a destination without creating an open redirect
Problem
The login flow accepts an arbitrary return URL, allowing attackers to send users
from a trusted sign-in page to a malicious site.
Before you start
Prefer a server-side return target stored in the session. If a destination must
travel through the form, restrict it to known local routes or an allowlist.
Submit a local destination
<form action="/login" method="post" ng-post="/login" on-error="errors = $res">
<input name="return_to" type="hidden" value="/account/orders" />
<label>Email <input name="email" type="email" autocomplete="username" /></label>
<label>
Password
<input name="password" type="password" autocomplete="current-password" />
</label>
<button type="submit">Sign in</button>
</form>
Server contract
Resolve the destination against an allowlist or accept only normalized local
paths. Reject protocol-relative URLs, encoded scheme tricks, control characters,
and destinations outside the application.
Failure path
Use a safe default page when the destination is missing or invalid. Never copy an
unchecked query or form value into the redirect response header.
Apply it now
Trace every return, next, redirect, and continue parameter to the server
code that validates its final destination.
Verify
Test a valid local path, absolute external URL, //host, encoded separators,
backslashes, and nested redirect parameters.
62 - Return one safe error shape
Make expected failures predictable without hiding their HTTP meaning
Problem
Every endpoint returns errors differently, so forms contain endpoint-specific
parsing and unexpected failures leak internal details.
Before you start
Define a small envelope with a safe message, optional field errors, a stable code,
and a request ID. Keep HTTP status as the primary category.
Bind predictable members
<form ng-post="/api/profile" on-error="error = $res">
<label>
Display name
<input name="name" required />
</label>
<p ng-if="error.fields.name">{{ error.fields.name }}</p>
<p ng-if="error.message" role="alert">{{ error.message }}</p>
<p ng-if="error.requestId">Support code: {{ error.requestId }}</p>
<button type="submit">Save</button>
</form>
Server contract
Keep the envelope stable across 401, 403, 409, 422, 429, and 5xx, but
preserve those statuses. Use machine codes only for behavior the client actually
needs; do not mirror exception class names.
Failure path
If an intermediary returns HTML or an empty body, show a generic safe fallback.
Error rendering must not throw another exception while parsing an unknown body.
Apply it now
Compare three endpoints’ failure bodies. Replace accidental differences with the
smallest common envelope rather than a large universal error object.
Verify
Exercise every documented status plus a proxy HTML error and malformed body.
Confirm the page remains usable and exposes no internal data.
63 - Make full-page and fragment responses explicit
Let one server route return the representation the request actually needs
Problem
The same URL serves normal navigation and an enhanced region update, but the
server sometimes returns a full page where a fragment is expected.
Before you start
Define an explicit request policy using Accept, a documented request header, or
separate routes. Do not infer intent from fragile user-agent checks.
Keep normal navigation available
<a
href="/account/orders"
ng-get="/account/orders"
data-target="#orders"
swap="innerHTML"
>
Refresh orders
</a>
<section id="orders" aria-live="polite">
<p>Current orders appear here.</p>
</section>
Server contract
Return a complete document for normal navigation and a valid application-owned
fragment for the enhanced request. Set Content-Type, cache variation, and error
responses consistently for both representations.
Failure path
Never swap a complete login, maintenance, or proxy page into the region. Detect
authentication and server failures by status before rendering the body.
Apply it now
Capture the normal and enhanced requests for one route. Write down exactly which
request signal chooses each representation.
Verify
Open the link normally, in a new tab, and through the enhanced interaction. Test
signed-out and maintenance responses in all three cases.
64 - Render user-authored rich text safely
Sanitize stored markup and keep it separate from application templates
Problem
Users can author formatted content, but inserting their HTML as an AngularTS
fragment could compile attacker-controlled directives or executable markup.
Before you start
Choose a maintained allowlist sanitizer and a deliberately small formatting
policy. Sanitize on the server before storage or rendering, not only in the browser.
Keep content inside an application-owned shell
<article>
<header><h1>{{ post.title }}</h1></header>
<div id="post-body" class="rich-text">
SERVER_RENDERED_SANITIZED_CONTENT
</div>
</article>
The shell may use AngularTS bindings. The user-authored body is sanitized content,
not a template and not a source of directives.
Server contract
Allow only required elements and attributes, normalize URLs, block executable
schemes, and add safe link behavior. Re-sanitize old content when policy defects
are fixed.
Failure path
Do not trust HTML because it came from your database, Markdown renderer, staff
account, or another service. Stored content can remain dangerous for years.
Apply it now
Trace one rich-text field from input to output and identify the exact sanitizer,
policy, version, and test suite protecting it.
Verify
Test scripts, event attributes, SVG, malformed nesting, encoded URLs, CSS escape
paths, and ordinary formatting that must remain intact.
65 - Add Content Security Policy in report-only mode
Measure executable-content dependencies before enforcing a restrictive policy
Problem
The application has no browser policy limiting where scripts, styles, frames, and
connections may come from.
Before you start
Inventory real production resources and choose a report endpoint that removes
sensitive URL and user data before storage.
Start by observing
Send Content-Security-Policy-Report-Only from the server. Begin with narrow
directives for default-src, script-src, style-src, img-src, font-src,
connect-src, frame-ancestors, base-uri, and form-action. Use nonces or
hashes for deliberate inline content instead of broad unsafe allowances.
Server contract
Generate a fresh nonce per response when used, attach it only to trusted server
scripts, and keep the policy consistent across normal, error, login, and
maintenance pages.
Failure path
Do not copy a restrictive example directly into enforcement. Missing origins can
break authentication, uploads, workers, WebTransport, or error reporting.
Apply it now
Run report-only policy on one production route, classify every violation, and
remove unnecessary resource origins before adding exceptions.
Verify
Exercise every application feature, browser extension noise, errors, and signed-out
pages. Enforce only after expected traffic produces no unexplained violations.
66 - Harden an authentication session cookie
Keep session credentials out of JavaScript and restrict browser delivery
Problem
Authentication state is readable by browser scripts or sent more broadly than
the application requires.
Before you start
Use the server framework’s maintained session implementation. Store only an opaque
session identifier in the browser, not authorization state or personal data.
Set the cookie at the server
Use Secure, HttpOnly, and an appropriate SameSite value. Keep Path and
Domain as narrow as the deployment allows. Rotate the session after sign-in and
privilege changes, and expire it on logout.
Server contract
Validate expiry and revocation on every authenticated request. Protect
state-changing cookie requests from CSRF. Avoid accepting the same credential in
both cookies and URLs.
Failure path
Do not fall back to local storage when a script cannot read an HttpOnly cookie;
that unreadability is the protection. A secure cookie still requires XSS defenses.
Apply it now
Inspect the production Set-Cookie response and one authenticated request. Explain
every attribute and remove any unnecessary domain or lifetime.
Verify
Test HTTPS, logout, expiry, session rotation, subdomains, cross-site requests, and
an attempted document.cookie read.
67 - Build a password-manager-friendly sign-in form
Use normal controls and autocomplete values before adding custom behavior
Problem
Custom sign-in controls prevent password managers, keyboard submission, or browser
security features from recognizing the form.
Before you start
Use a normal server POST endpoint over HTTPS. Keep labels, control names, and a
submit button in the document.
Use standard autocomplete semantics
<form action="/login" method="post" ng-post="/login" on-error="errors = $res">
<label>
Email
<input name="email" type="email" autocomplete="username" required />
</label>
<label>
Password
<input
name="password"
type="password"
autocomplete="current-password"
required
/>
</label>
<p ng-if="errors.message" role="alert">{{ errors.message }}</p>
<button type="submit">Sign in</button>
</form>
Server contract
Use a generic authentication error, rate-limit abuse, rotate the session after
success, and keep account recovery separate. New-password forms use
autocomplete="new-password".
Failure path
Do not disable paste, invent text controls that imitate passwords, reveal whether
an account exists, or clear the username after a failed attempt.
Apply it now
Run the sign-in form with a browser password manager and keyboard only. Remove
custom behavior that blocks recognition or submission.
Verify
Test saved credentials, multiple accounts, failed login, Enter submission,
password reveal controls, and sign-in with JavaScript disabled.
68 - Keep secrets out of errors and logs
Return safe diagnostics while preserving enough context to trace failures
Problem
Request bodies, headers, URLs, or exceptions can copy credentials and personal
data into browser errors and long-lived logs.
Before you start
Classify credentials and sensitive fields. Define allowlisted log fields instead
of trying to redact an unlimited set after serialization.
Show only a safe support reference
<form ng-post="/api/connections" on-error="connectionError = $res">
<label>API key <input name="apiKey" type="password" required /></label>
<button type="submit">Connect</button>
<p ng-if="connectionError" role="alert">
Connection failed. Support code: {{ connectionError.requestId }}
</p>
</form>
Server contract
Never return or log the submitted key. Log an allowlisted operation name, safe
resource ID, status, duration, and request ID. Redact downstream exception data
before it crosses service boundaries.
Failure path
Do not place secrets in query strings, analytics events, breadcrumbs, DOM data
attributes, or client-side persistent state. Treat screenshots as exportable data.
Apply it now
Trigger one failed credential submission in a safe environment and follow it
through browser tools, proxy logs, application logs, and error reporting.
Verify
Search every captured artifact for the exact submitted secret and related personal
values. The search must return nothing.
69 - Focus the first field rejected by the server
Move keyboard users directly to a field-level validation problem
Problem
The server returns validation errors below the visible area and keyboard users do
not know which control needs attention.
Before you start
Expose important controls with ng-el and return field errors using control names.
Focus only after the failed response, not while the user is typing.
Focus a known field after rejection
<form
ng-post="/account/profile"
on-error="errors = $res; emailInput.focus()"
>
<label>
Email
<input ng-el="emailInput" name="email" type="email" required />
</label>
<p ng-if="errors.email" id="email-error">{{ errors.email }}</p>
<button type="submit">Save profile</button>
</form>
For several fields, let the response handler choose the first rejected control in
document order. Keep the visible message associated with its input.
Server contract
Return stable field names and safe messages with 422. Form-level failures must
target a form summary rather than an arbitrary input.
Failure path
Do not steal focus for background refreshes or move it repeatedly as messages
render. If the control is hidden, reveal it before focusing.
Apply it now
Submit the longest form from its last field and note where focus remains. Move it
to the first actionable server error.
Verify
Use keyboard and screen reader navigation with one error, several errors, a hidden
section, and a form-level server failure.
70 - Turn an empty result into the next action
Distinguish no data from loading and failure, then explain what to do
Problem
An empty collection renders a blank region that looks broken and gives no path
forward.
Before you start
Separate first-use emptiness, filter emptiness, permission limits, loading, and
request failure. They require different messages and actions.
Render the empty case deliberately
<section>
<ul ng-if="projects.length > 0">
<li ng-repeat="project in projects">{{ project.name }}</li>
</ul>
<div ng-if="projects.length === 0">
<h2>No projects yet</h2>
<p>Create the first project to organize this work.</p>
<a href="/projects/new">Create a project</a>
</div>
</section>
Server contract
Return an empty collection as a successful result. Do not use 404 for a valid
collection with no records. Preserve active filters so the empty explanation can
offer clearing them when appropriate.
Failure path
Never render the empty state while the request failed or has not completed; doing
so falsely tells the user that their data is gone.
Apply it now
Find one blank list and write the exact reason it can be empty. Add the next useful
action for that reason.
Verify
Test first use, filters with no matches, permission-limited data, slow loading,
request failure, and a non-empty result.
71 - Preserve focus when replacing server HTML
Keep the trigger stable or move focus to a deliberate result heading
Problem
A fragment replacement removes the focused element or leaves keyboard users
outside the newly displayed content.
Before you start
Choose whether focus should remain on a stable trigger or move to the result. Add
a programmatically focusable heading only when moving focus is useful.
Keep the target stable and focus its heading
<section>
<button
ng-get="/account/summary"
data-target="#summary-body"
swap="innerHTML"
on-success="summaryHeading.focus()"
>
Load summary
</button>
<article>
<h2 ng-el="summaryHeading" tabindex="-1">Account summary</h2>
<div id="summary-body"><p>Summary not loaded.</p></div>
</article>
</section>
Server contract
Return only the body fragment so the stable heading and its ng-el reference are
not removed. Use valid application-owned HTML.
Failure path
Keep focus on the trigger after failure and announce the error nearby. Do not move
focus for silent prefetch or background refreshes.
Apply it now
Run one fragment swap using only the keyboard and observe the active element before
and after. Make that transition deliberate.
Verify
Test success, failure, repeated replacement, screen reader announcements, and a
user who activates the control with keyboard or pointer.
72 - Deliver a feature flag with the server page
Avoid a second request and keep authorization separate from rollout
Problem
The page waits for a second request before deciding which small interface variation
to show, causing flicker and inconsistent first rendering.
Before you start
Evaluate the flag on the server using the authenticated request and rollout policy.
Treat the result as presentation, never as permission.
Initialize the evaluated result
<section ng-init="flags = { compactCheckout: false }">
<a ng-if="flags.compactCheckout" href="/checkout/compact">
Use compact checkout
</a>
<a ng-if="!flags.compactCheckout" href="/checkout">
Continue to checkout
</a>
</section>
Render the evaluated boolean into the server template. Do not expose targeting
rules, experiment secrets, or unrelated flags.
Server contract
Keep both paths authorized and compatible with current data. Record the evaluated
variant with operational events needed to compare rollout health.
Failure path
Choose a safe default when flag evaluation fails. Removing a UI path does not make
its endpoint unavailable to direct requests.
Apply it now
Find one startup request used only for feature configuration. Move the required
evaluated values into the initial server response.
Verify
Test both variants, evaluation failure, direct endpoint access, rollback, cached
pages, and users moving between rollout groups.
73 - Record who changed sensitive data
Write an audit event in the same server operation as the state change
Problem
A sensitive record changed, but the application cannot explain who changed what,
when, or through which operation.
Before you start
Define which domain actions require audit history, who may read it, retention,
redaction, and the difference between audit events and diagnostic logs.
Ask for context when the domain needs it
<form ng-post="/api/accounts/42/suspend" on-error="errors = $res">
<label>
Reason for suspension
<textarea name="reason" required></textarea>
</label>
<button type="submit">Suspend account</button>
</form>
Server contract
Derive actor, tenant, request ID, time, and affected resource on the server. Record
the domain action and safe before-and-after facts in the same transaction or a
reliable transactional outbox.
Failure path
Do not trust actor or timestamp fields from the form. Avoid placing credentials,
unbounded record snapshots, or unnecessary personal data in the audit event.
Apply it now
Choose one permission, billing, account, or data-export action and prove its state
change cannot commit without its required audit event.
Verify
Test success, transaction rollback, retry, impersonation where supported, redaction,
retention, and unauthorized audit-log access.
74 - Roll out a request contract without breaking old pages
Expand, migrate, and contract server behavior across cached browser versions
Problem
A deployment changes a field or response shape while users still have pages,
workers, or tabs running code from the previous release.
Before you start
Measure how long old clients and cached pages remain active. Identify producers,
consumers, integrations, queued jobs, and persisted data using the contract.
Use an expand-and-contract rollout
First make the server accept old and new requests. Then deploy clients that use
the new shape. Observe adoption and errors. Migrate stored data and background
producers. Remove old behavior only after the supported compatibility window.
Server contract
Ignore additive response fields safely, preserve field meaning, and use a new field
or endpoint for incompatible semantics. Keep rollback possible throughout rollout.
Failure path
Do not rename or repurpose a field in one deployment, assume all tabs refresh, or
couple database migration completion to a single frontend release instant.
Apply it now
Take the next contract change and write its expand, client migration, observation,
data migration, and removal steps before implementation begins.
Verify
Run old client against new server, new client against compatible old behavior,
mixed workers, rollback, delayed jobs, and a tab kept open across deployment.
75 - Find a beginner recipe
Start with inspection, server data, DOM wiring, events, forms, and HTTP
Start here
Work through these recipes in order until you reach the problem you need to solve.
Each one assumes only the earlier browser and HTML concepts.
Choose the next area
Continue with the forms, HTTP, DOM, navigation, security, or performance finder
instead of reading the cookbook from top to bottom.
76 - Find a form recipe
Solve validation, uploads, conflicts, multi-step flows, and sensitive input
Build the basic flow
Protect the write
77 - Find an HTTP recipe
Choose statuses, bodies, caching, retries, fragments, and origin policy
Define the response first
Control requests
Handle deployment boundaries
78 - Find a DOM recipe
Select, update, replace, animate, and focus application-owned elements
Wire elements to scope
Build common interactions
Keep updates usable
79 - Find a security recipe
Protect requests, sessions, redirects, content, credentials, and permissions
Protect server operations
Protect browser content
Protect destructive actions
80 - Find a performance recipe
Reduce requests, rendering, transfer, and main-thread work after measuring
Reduce network work
Reduce rendering work
Move specialized work
Reduce shipped JavaScript
81 - Find a data correctness recipe
Preserve identity, precision, concurrency, validation, and audit history
Represent values correctly
Protect concurrent changes
Preserve history and compatibility
82 - Find a navigation recipe
Keep URLs, server pages, router states, sessions, and redirects coherent
Start with server navigation
Add browser-owned state where needed
83 - Find an operations recipe
Trace failures, survive outages, control rollout, and explain sensitive changes
Diagnose a request
Operate degraded systems
Change production safely
84 - Find an advanced browser recipe
Use effects, viewport events, streams, workers, WebAssembly, and specialized UI
React to browser and scope changes
Add specialized execution
Build specialized interaction
Integrate another framework
85 - Exchange live messages with WebSocket
Build a bidirectional chat channel with reconnect and cleanup
Problem
The browser must send messages while the server pushes new messages over the
same long-running connection. Repeated HTTP requests or SSE only solve half of
that conversation.
Before you start
Use WebSocket only when both sides need to send independently. Prefer SSE when
the browser only receives updates, and ordinary HTTP when messages are
infrequent.
AngularTS exposes
WebSocketService,
WebSocketConfig, and the
managed
WebSocketConnection.
The managed connection handles message decoding, retry policy, heartbeats, and
explicit cleanup.
For a custom production runtime, include websocketModule from
@angular-wave/angular.ts/runtime/realtime. Do not include the aggregate
realtimeModule unless the same runtime also uses SSE and WebTransport.
Open a chat connection
Keep the connection in one controller or service. Update the scope-owned view
model from callbacks and close the connection with the scope.
class ChatController {
static $inject = ['$websocket', '$scope'];
messages = [];
draft = '';
status = 'connecting';
constructor($websocket, $scope) {
this.socket = $websocket('wss://example.com/chat', {
protocols: ['chat.v1'],
retryDelay: 1_000,
maxRetries: 10,
onOpen: () => ($scope.chat.status = 'connected'),
onMessage: (message) => $scope.chat.messages.push(message),
onClose: () => ($scope.chat.status = 'reconnecting'),
});
$scope.on('$destroy', () => this.socket.close());
}
send() {
const text = this.draft.trim();
if (!text || this.status !== 'connected') return;
this.socket.send({ type: 'chat.message', id: crypto.randomUUID(), text });
this.draft = '';
}
}
angular.createModule('chat', []).controller('ChatController', ChatController);
send() serializes the object as JSON. Give every client message an ID so the
server can ignore a duplicate after a reconnect. Validate the message type,
size, authorization, and payload on the server before broadcasting it.
<section ng-controller="ChatController as chat">
<p>Connection: {{ chat.status }}</p>
<ol aria-live="polite">
<li ng-repeat="message in chat.messages">{{ message.text }}</li>
</ol>
<form ng-submit="chat.send()">
<label>Message <input ng-model="chat.draft" /></label>
<button type="submit" ng-disabled="chat.status !== 'connected'">
Send
</button>
</form>
</section>
Use wss:// from an HTTPS page. At the server or reverse proxy, enable the HTTP
upgrade, authenticate the handshake, check the request origin, cap frame and
queue sizes, and enforce idle timeouts. Cookies can authenticate the handshake;
a short-lived connection ticket is safer than a reusable token in the URL.
Failure path
A reconnect creates a new transport, not a continuation of the old one. The
server may have accepted a message before the connection disappeared, so retry
only messages with stable IDs and deduplicate them server-side. Resume incoming
messages from a server cursor when gaps matter.
Do not queue without a limit while offline. Disable writes or keep a small,
visible outbox. When the queue reaches its limit, reject new entries and tell
the user instead of allowing memory use to grow.
Apply it now
Choose one interaction where either side must speak without waiting for the
other, such as chat, collaborative presence, or live control. Define the message
types and IDs first, then add reconnect and resume behavior before building the
interface.
Verify
Send a valid message, malformed JSON, an oversized frame, and an unauthorized
message. Interrupt the connection before and after server acceptance. Confirm
duplicates are ignored, missing messages resume, the offline queue stays
bounded, reconnect attempts stop after scope destruction, and the production
proxy keeps the upgrade open.
86 - Integrate AngularTS with another framework
Choose DOM ownership, messages, observed attributes, or shared reactive state
Problem
An AngularTS module must coexist with a web component or another UI framework
without both runtimes fighting over the same DOM and state.
Before you start
Give each framework its own root element. One framework owns rendering and
cleanup inside that root. Integrate at the root boundary instead of compiling
the same subtree twice.
Choose the smallest bridge
| What crosses the boundary | Use |
|---|
| A transient message that something happened | The event bus |
| State reflected by a custom element attribute | ng-observe |
| Current domain state needed by both sides | A shared model |
Use browser custom events directly when one element and its immediate owner are
the only participants. Use the event bus when producers and consumers should not
know each other. Use a model when consumers need the current value, not only a
notification that it changed.
Failure path
Do not use the event bus as a state store, poll DOM attributes that can be
observed, or let two frameworks render the same children. Every subscription,
observer, and external framework root needs one clear lifecycle owner.
Apply it now
Choose one existing cross-framework callback and classify what crosses the
boundary: DOM state, a transient event, or durable current state. Replace the
custom glue with the matching bridge.
Verify
Mount and unmount both roots repeatedly. Confirm messages are delivered once,
attribute state remains current, models update both consumers, and no listener
or observer survives its owner.
87 - Send messages across framework roots
Use the application event bus for asynchronous cross-boundary notifications
Problem
A component in another framework needs to notify an AngularTS module without
holding its controller, scope, or DOM implementation.
Before you start
Name the event after something that happened, such as product:selected. Keep
the payload small and define its shape at the integration boundary.
Publish outside and subscribe inside AngularTS
const app = angular.createModule('catalog', []);
app.controller('SelectionController', [
'$eventBus',
'$scope',
function SelectionController($eventBus, $scope) {
this.selected = undefined;
$eventBus.subscribe(
'product:selected',
(product) => (this.selected = product),
$scope,
);
},
]);
externalCatalog.onSelected((product) => {
angular.eventBus.publish('product:selected', product);
});
The injected $eventBus and angular.eventBus expose the same application bus.
Delivery is asynchronous through a microtask and listeners run in subscription
order. Passing a scope as the subscription context removes the listener when the
scope is destroyed and skips queued delivery to that destroyed scope.
Use the right event mechanism
Use scope events for parent and child communication in one scope tree. Use DOM
custom events for an element and its owner. Use the event bus for decoupled
modules, workers, realtime services, and non-Angular framework roots.
Failure path
The bus does not retain the latest payload and does not own state. A late
subscriber does not receive an earlier event. Subscriber failures are sent to
the configured exception handler.
Apply it now
Replace one global callback registry with a named event bus topic. Give the
subscription a scope owner or call the returned unsubscribe function explicitly.
Verify
Publish before and after subscribing, destroy the owning scope before queued
delivery, publish twice, and mount the external root twice. Confirm the expected
listener runs once per publication and is removed on teardown.
88 - Read Fluent UI custom-element state with ng-observe
Mirror reflected web-component attributes into AngularTS scope
Problem
A custom element owns its internal behavior and reports current state by
changing an attribute. AngularTS needs that value without taking over the
element’s DOM.
Before you start
Confirm the component reflects the required state to an attribute. ng-observe
watches attributes with MutationObserver; it does not poll JavaScript-only
properties.
Observe the Fluent UI attribute
This example is taken from the repository’s Fluent UI observe demo.
<section>
<fluent-select
ng-observe-current-value="selectedSection"
title="Select a section"
>
<fluent-option value="Beginning">Beginning</fluent-option>
<fluent-option value="Middle">Middle</fluent-option>
<fluent-option value="End">End</fluent-option>
</fluent-select>
<p>Selected section: {{ selectedSection }}</p>
</section>
ng-observe-current-value="selectedSection" watches the current-value
attribute and writes its value to selectedSection on the current scope. If the
directive value is omitted, the camel-cased attribute name becomes the scope
name: ng-observe-aria-checked writes to ariaChecked.
The same shipped demo observes activeid on fluent-tabs, value on
fluent-radio-group, and aria-checked on fluent-switch.
Failure path
Use the exact reflected attribute name documented by the web component. If the
component changes only a JavaScript property, use its custom event or an adapter
instead. The observer disconnects automatically when the scope is destroyed.
Apply it now
Open the custom element in the Elements panel and interact with it. Identify the
attribute that actually changes, then bind only that attribute with
ng-observe.
Verify
Check the initial reflected value, change the selection, set the same value
again, and destroy the AngularTS root. Confirm scope follows real attribute
changes and the observer is disconnected on teardown.
89 - Share current state through an application model
Give AngularTS and another framework one reactive domain object
Problem
Two framework roots need the current shopping-cart state. Sending only
item:added events makes late consumers reconstruct state and handle missed
messages.
Before you start
Choose one owner for the model and its mutations. Retrieve the same registered
model from the AngularTS application injector instead of copying it into each
framework store.
const shop = angular.createModule('shop', []);
shop.model('cart', () => ({
items: [],
currency: 'EUR',
}));
shop.controller('CartSummary', [
'cart',
function CartSummary(cart) {
this.cart = cart;
},
]);
Inject cart into AngularTS controllers, services, or component views. A
non-Angular root can retrieve the same instance from the injector associated
with the shared application root.
const host = document.querySelector('[ng-app="shop"]');
const cart = angular.getInjector(host).get('cart');
externalProductGrid.onAdd((product) => {
cart.items.push(product);
});
Nested object and array mutations are reactive. AngularTS interpolation,
watchers, repeats, and component views consuming the model receive the updated
cart. Framework roots managed by the same application context receive the same
named model instance.
Render the shared value
<aside ng-controller="CartSummary as summary">
<p>Cart items: {{ summary.cart.items.length }}</p>
<ul>
<li ng-repeat="item in summary.cart.items">{{ item.name }}</li>
</ul>
</aside>
Failure path
Do not maintain a second cart and synchronize it with event messages. Do not put
view-only details such as open menus or focused controls into the domain model.
Use the event bus for transient notifications and the model for current state.
Apply it now
Choose one concept shared across roots, such as cart, session, playback, or
workspace selection. Register it once and replace copied stores with that model.
Verify
Add an item from each framework, mount a consumer after items already exist,
mutate a nested array, and destroy one root. Confirm every live consumer sees
the same cart and destroyed views stop observing it.
90 - Build a smaller production runtime
Compose, bundle, measure, and verify only the AngularTS features an application uses
Problem
The production JavaScript bundle is larger than its measured budget even though
the application uses only a small part of AngularTS. Importing the full runtime
ships directives and services that this page never uses.
Before you start
Optimize the server response, images, fonts, and application code before
maintaining a custom runtime. Use this recipe only after a production bundle
report shows that unused AngularTS features are material.
createAngular accepts an
AngularComposition and
returns an AngularRuntime. Its
core contains compilation, scopes, controllers, expressions, interpolation, and
exception handling. You explicitly add every directive, service, filter, and
optional RuntimeModule used by
your application.
Create one runtime entry
Create src/runtime.ts. Import package subpaths directly so the bundler can
discard modules that are not reachable from this entry.
import { createAngular } from '@angular-wave/angular.ts/runtime';
import { ngControllerDirective } from '@angular-wave/angular.ts/directive/controller';
import { ngEventDirectives } from '@angular-wave/angular.ts/directive/events';
const angular = createAngular({
directives: {
ngController: ngControllerDirective,
ngClick: ngEventDirectives.ngClick,
},
});
class CounterController {
count = 0;
increase() {
this.count += 1;
}
}
angular
.createModule('counterApp', [])
.controller('CounterController', CounterController);
const root = document.querySelector('#counter-app');
if (!root) throw new Error('Missing counter application root');
angular.bootstrap(root, ['counterApp']);
export { angular };
Register the exact directive names used by the HTML. A custom runtime does not
discover ng-click from a template and add it automatically.
<section id="counter-app" ng-controller="CounterController as counter">
<strong>{{ counter.count }}</strong>
<button type="button" ng-click="counter.increase()">Increase</button>
</section>
Import optional modules only when the application uses them. For example, add
routerModule from @angular-wave/angular.ts/runtime/router for browser-owned
routing. Import sseModule, websocketModule, or webTransportModule from
@angular-wave/angular.ts/runtime/realtime when only one live transport is
needed. Use machineModule or workflowModule from
@angular-wave/angular.ts/runtime/orchestration instead of their aggregate when
only one orchestration feature is needed. Do not import the package root from
this entry; it restores the general runtime and its auto-bootstrap behavior.
Build it with Vite
Point the production build at the runtime entry. Keep property names intact:
HTML expressions and dependency-injection tokens refer to them by name.
import { defineConfig } from 'vite';
export default defineConfig({
build: {
target: 'es2022',
minify: 'esbuild',
sourcemap: 'hidden',
rollupOptions: {
input: 'src/runtime.ts',
output: {
entryFileNames: 'assets/runtime-[hash].js',
},
},
},
});
Run npm exec vite build. Serve the hashed file with a long-lived immutable
cache policy and deploy the generated HTML or manifest that references its new
hash. Upload the hidden source map to error monitoring, but do not serve it as a
public asset unless that is intentional.
Vite’s default minifier does not mangle object properties. If a different
minifier is introduced, do not property-mangle controller members, model
members, directive attributes, injection tokens, or names read by server HTML.
Use static $inject or array injection for every dependency-bearing function.
Measure the artifact
Compare the custom build with the existing production build under the same
target, minifier, and compression settings. Record raw, gzip, and Brotli sizes;
the compressed transfer size is the useful network budget. For example, run
gzip -9 -c dist/assets/runtime-*.js | wc -c after the build.
Set a CI budget only after recording a stable baseline. Fail on unexpected
growth, but allow an intentional increase when it removes more application code
or improves runtime behavior. Bundle size alone is not a performance result;
also measure startup and the interaction that motivated this work.
Failure path
Server-rendered and remotely loaded templates are not visible to the bundler.
Inventory their directives explicitly and compile representative responses in
tests. If templates or plugins can introduce unknown AngularTS features, ship
the standard runtime for that application instead of discovering missing
directives in production.
Do not create several nearly identical runtime entries. Share one composition
per application architecture and add a feature deliberately when a tested
template needs it. A few saved bytes are not worth inconsistent behavior across
pages.
Apply it now
Build the current application and record its compressed runtime size. List the
directives, filters, services, and optional modules used by production HTML.
Create one explicit runtime entry, remove one unused feature group, and compare
both size and startup measurements before removing anything else.
Verify
Compile every shipped server fragment and route template with the production
runtime. Exercise bootstrap, forms, errors, lazy content, and teardown from the
minified artifact. Confirm omitted services fail in a test, source maps resolve
in monitoring, the server sends compressed hashed assets, and the CI size budget
detects an intentional temporary increase.
91 - Add behavior to server-rendered HTML
Enhance one part of a page without building an SPA
Problem
The server already renders a useful page. One part needs client-side behavior,
but the page should still work when JavaScript fails or loads slowly.
Recipe
Keep navigation, forms, and initial content in HTML. Put AngularTS on the
smallest element that needs it.
<main>
<h1>Search products</h1>
<form
action="/products"
method="get"
ng-app="catalog"
ng-controller="SearchController as search"
>
<label for="query">Search</label>
<input id="query" name="q" ng-model="search.query" />
<span aria-live="polite" ng-bind="search.message()"></span>
<button type="submit">Search</button>
</form>
</main>
const app = angular.createModule('catalog', []);
app.controller('SearchController', function SearchController() {
this.query = '';
this.message = () => {
const count = this.query.trim().length;
return count === 0 ? 'Enter a search term' : count + ' characters';
};
});
The form remains a normal GET request. AngularTS adds feedback, but does not
own navigation or the initial result page.
Why this works
The server keeps the model, URLs, validation, authorization, and HTML together.
The browser handles only immediate feedback.
This gives users useful content before AngularTS starts and leaves a working
fallback if the script does not run. It also avoids creating a second model that
must stay synchronized with the server.
If the browser must take over
Keep router content server-rendered while the server can still own the model.
Move the model into the browser only when a complex feature needs independent
client ownership, such as substantial offline work or a long-lived editing
workspace.
At that point the browser owns model consistency and compilation. Typed
programmatic views are an optional way to type-check model-to-view bindings;
HTML templates remain supported. REST services, explicit cache ownership, and a
browser security policy become primary at the same point. Before that threshold,
prefer normal HTTP forms and server-rendered fragments.
Make that choice per module. A login form can stay as server-rendered HTML while
a separate dashboard ng-app owns a browser model. The dashboard can choose a
typed programmatic view without requiring the rest of the application to adopt
the same view style.
Verify
- Load the page with JavaScript disabled.
- Submit the form and confirm the server returns results.
- Enable JavaScript and confirm the live message appears.
- Confirm the same URL and server response are used in both cases.
Avoid
Do not fetch the initial page through AngularTS only to reproduce HTML the
server can already send. Use a client-owned route only when the interaction
truly needs long-lived browser state. See Keep the application on the
server.
92 - Make a component view reactive
Fix state changes that do not update a typed component view
Problem
A component changes its controller state, but a value in its component view
stays frozen at the initial value.
Recipe
Pass a reader function for values that can change.
import { button, output } from '@angular-wave/angular.ts';
class CounterController {
count = 0;
}
const app = angular.createModule('counter', []);
app.component<CounterController>('counter', {
controller: CounterController,
view: ({ controller }) => {
return [
button(
{
type: 'button',
onclick: () => {
controller.count += 1;
},
},
'Add',
),
output(() => controller.count),
];
},
});
Use () => controller.count, not controller.count, for the changing child.
Why this works
controller.count reads the value while the view is being created. The view
receives one number and has nothing to read later.
() => controller.count gives the runtime a reader. AngularTS tracks the
properties read by that function and updates the affected DOM when they change.
The same rule applies to reactive properties:
button(
{
disabled: () => !controller.canSubmit,
},
'Save',
);
Verify
- Click Add several times.
- Confirm the existing
output node changes. - Inspect the DOM and confirm the component is not rebuilt for each click.
Avoid
Do not wrap constant strings or numbers in functions. Readers are for values
that can change. Keep static children static.
93 - Keep repeated rows stable
Preserve row DOM when refreshed data represents the same records
Problem
Refreshing a collection replaces every object. ng-repeat then rebuilds rows,
which loses focus, selection, local element state, and animation continuity.
Recipe
Reconcile incoming records with the existing objects before replacing the
collection.
interface User {
id: string;
name: string;
}
function reconcileUsers(current: User[], incoming: User[]): User[] {
const existing = new Map(current.map((user) => [user.id, user]));
return incoming.map((next) => {
const user = existing.get(next.id);
if (!user) {
return next;
}
Object.assign(user, next);
return user;
});
}
Use the reconciled array after a refresh:
controller.users = reconcileUsers(controller.users, response.data);
Render it normally:
<ul>
<li ng-repeat="user in users">
<input ng-model="user.name" />
</li>
</ul>
Why this works
ng-repeat tracks objects by identity. Reusing the object for the same id
lets AngularTS reuse that row. New records get new rows, removed records lose
their rows, and existing records keep their DOM.
The server ID decides which record is the same. The repeat directive still uses
its built-in identity behavior.
Verify
- Focus an input in one row.
- Refresh the collection with new objects carrying the same IDs.
- Confirm focus stays in that row.
- Add and remove records and confirm only those rows change.
Avoid
Do not reconcile records that have no stable identity. Do not reuse an object
when the server ID changed. For small primitive arrays where row state does not
matter, replacing the collection is simpler.
94 - Stop hidden UI from doing work
Choose between removing UI and only hiding it
Problem
A closed panel still runs watchers, observers, timers, or network work because
CSS hides it without removing it.
Recipe
Use ng-if when closing the UI should destroy it.
<button type="button" ng-click="detailsOpen = !detailsOpen">
Toggle details
</button>
<account-details ng-if="detailsOpen"></account-details>
Use ng-show only when the DOM and its state must stay alive:
<video-player ng-show="playerOpen"></video-player>
Why this works
ng-if removes the subtree and destroys its child scope. Reopening it creates a
fresh subtree.
ng-show changes visibility. The subtree, scope, form state, media state, and
watchers stay alive.
Choose based on what should happen while the UI is closed, not on which
directive is shorter.
Verify
- Open and close the panel repeatedly.
- Confirm
ng-if runs component cleanup when the panel closes. - Confirm hidden panels no longer issue requests or handle browser events.
- If state must survive, confirm the
ng-show version preserves it.
Avoid
Do not use ng-if for a control that opens and closes constantly when
rebuilding it is expensive. Do not use ng-show for a large inactive subtree
that keeps doing work.
95 - Ignore stale request results
Keep older responses from replacing newer search results
Problem
A user searches for cat, then quickly searches for caterpillar. The second
request finishes first. The slower cat response arrives later and replaces the
correct results.
Recipe
Give each request a sequence number. Apply a response only when it belongs to
the newest request.
app.controller('SearchController', function SearchController($http) {
let latestRequest = 0;
this.results = [];
this.loading = false;
this.error = '';
this.search = async (query) => {
const request = ++latestRequest;
this.loading = true;
this.error = '';
try {
const response = await $http.get('/api/search', {
params: { q: query },
});
if (request === latestRequest) {
this.results = response.data;
}
} catch (error) {
if (request === latestRequest) {
this.error = 'Search failed';
}
} finally {
if (request === latestRequest) {
this.loading = false;
}
}
};
});
Why this works
Network responses can arrive in any order. The sequence number records which
request currently owns the screen. Older requests may finish, but they cannot
change results, errors, or loading state.
This check is still needed when the transport supports cancellation. A request
can finish just before cancellation reaches it.
Verify
- Delay short-query responses in the browser network tools.
- Search for a short term, then immediately search for a longer term.
- Confirm only the longer term changes the screen.
- Repeat with a failed older request and confirm it does not replace the latest
state.
Avoid
Do not use one shared loading boolean without checking which request finished.
An older request can otherwise hide the loading indicator for a newer one.
96 - Clean up browser resources
Stop listeners, observers, and timers when a view is removed
Problem
A component adds a browser listener or timer. After navigation or ng-if
removes the component, the callback still runs and keeps old state in memory.
Recipe
Create the resource inside the component view and register its cleanup at the
same time.
import { time } from '@angular-wave/angular.ts';
class ClockController {
now = new Date();
}
app.component<ClockController>('clock', {
controller: ClockController,
view: ({ controller, onDestroy }) => {
const timer = window.setInterval(() => {
controller.now = new Date();
}, 1_000);
onDestroy(() => {
window.clearInterval(timer);
});
return time(() => controller.now.toLocaleTimeString());
},
});
For browser listeners, an AbortController keeps several listeners under one
cleanup:
const events = new AbortController();
window.addEventListener('resize', updateSize, {
signal: events.signal,
});
onDestroy(() => events.abort());
Why this works
Setup and cleanup live next to each other, so a later edit is less likely to add
one without the other. AngularTS runs the registered disposer when it removes
the component view.
Verify
- Open and remove the component several times.
- Confirm only one timer or listener runs while it is visible.
- Confirm no callback runs after removal.
- Use the browser memory tools to confirm removed controllers can be collected.
Avoid
Do not store component timers in a global list. Do not wait for a later route to
clean up resources created by the current component.
97 - Find the slow part of an interaction
Measure whether delay comes from JavaScript, layout, rendering, or the network
Problem
An interaction feels slow, but changing watchers or rendering code without a
trace only moves work around.
Recipe
Record one interaction and optimize the longest measured task.
- Reproduce the problem with realistic data and a production build.
- Open the browser Performance panel.
- Start recording immediately before the interaction.
- Stop after the screen reaches its final state.
- Find the longest main-thread task or network wait.
- Expand that task until you can name the function, layout, paint, or request
responsible.
- Change one cause and record the same interaction again.
Add marks when the trace does not show where application work starts:
performance.mark('results:update:start');
controller.results = nextResults;
requestAnimationFrame(() => {
performance.mark('results:update:painted');
performance.measure(
'results:update',
'results:update:start',
'results:update:painted',
);
});
Read the result in the browser Performance panel or with:
performance.getEntriesByName('results:update');
Why this works
A slow click can come from unrelated causes: request latency, JavaScript, style
calculation, layout, paint, image decoding, or too much DOM. Each cause needs a
different fix.
Measuring through the next animation frame includes the browser work needed to
show the change, not only the assignment.
Verify
Keep the before and after traces. Compare the same data, browser, build mode,
and interaction. Accept the change only when the measured delay improves without
moving the cost to another step.
Avoid
Do not use development timings as release numbers. Do not remove reactivity,
accessibility, or cleanup code because it looks expensive without a trace that
shows it is the cause.
98 - Render every request state
Show loading, success, empty, and error states without contradictory UI
Problem
A request-driven screen shows stale content during loading, treats an empty
result as an error, or leaves the loading indicator visible after failure.
Recipe
Model request status separately from its data.
app.controller('OrdersController', function OrdersController($http) {
this.status = 'idle';
this.orders = [];
this.load = async () => {
this.status = 'loading';
try {
const response = await $http.get('/api/orders');
this.orders = response.data;
this.status = this.orders.length === 0 ? 'empty' : 'ready';
} catch (error) {
this.status = 'error';
}
};
});
Render each state once:
<section aria-live="polite">
<p ng-if="orders.status === 'loading'">Loading orders...</p>
<button ng-if="orders.status === 'error'" ng-click="orders.load()">
Try again
</button>
<p ng-if="orders.status === 'empty'">No orders yet.</p>
<ul ng-if="orders.status === 'ready'">
<li ng-repeat="order in orders.orders" ng-bind="order.number"></li>
</ul>
</section>
Why this works
One status value prevents impossible combinations such as loading and error
appearing together. Empty data is a successful response, so it gets its own
state instead of sharing the error path.
Keeping the last successful data is also valid. If you do that, show a separate
refresh indicator and make it clear that the visible data is old.
Verify
Test all four responses:
- A delayed success shows loading, then results.
- An empty success shows the empty message.
- A failure shows retry and no loading indicator.
- Retry can move from error through loading to ready or empty.
Avoid
Do not derive loading from orders.length === 0; empty results are not still
loading. Do not clear an error only after the next request succeeds; clear it
when retry starts.