This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

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.

Needed resultStart with
The server already knows the final HTMLSwap server-rendered HTML
One interaction needs JSON and local template stateBuild and submit forms
Several features share records and write operationsRead, write, and prefetch REST data
The browser must own URLs, history, and nested viewsAdd browser-owned routes
The server pushes one-way updatesReceive live server events
The browser and server both send live messagesExchange WebSocket messages
Existing computation blocks rendering after measurementMove measured CPU work to a worker
The measured runtime contains unused featuresBuild a smaller production runtime

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

  1. Debug a running application
  2. Select an element with ng-el
  3. Pass server data without another request
  4. Update existing DOM from state
  5. Replace DOM from state
  6. Swap server-rendered HTML
  7. Run effects outside the DOM
  8. Handle events
  9. Build and submit forms
  10. Read, write, and prefetch REST data
  11. Authenticate requests
  12. Cache REST reads
  13. 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.

ProblemRecipe
Edit one record without leaving the pageEdit a server record in place
Delete one row without rebuilding its listDelete a row after server confirmation
Search without stale responses winningSearch while the user types
Load choices from another fieldLoad one select from another
Show a reversible change immediatelyUpdate immediately and roll back
Upload a file and allow cancellationCancel a file upload
Choose status, content type, and bodyDesign an HTTP response

Load and navigate faster

ProblemRecipe
Data should be ready before clickPrefetch data before the user clicks
More rows should load near the viewportLoad the next page near the viewport
Stable lookup lists repeatCache reference data safely
A private request finds an expired sessionReturn users to login
A dialog needs current server contentLoad a dialog from the server

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.

ProblemRecipe
A server page needs one interactive widgetAdd behavior without making an SPA
A component changed state but DOM did notMake a component view reactive
Refreshed rows lose focusKeep repeated rows stable
Hidden UI keeps doing workStop hidden UI work
An older request winsIgnore stale request results
A listener survives its UIClean up browser resources
An interaction feels slowFind the slow part
Request states are incompleteRender every request state

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:

scope.query = 'boots';

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

  1. Select the element whose text or value you want to change.
  2. Read its scope or controller and find the matching property.
  3. Change that property in the console.
  4. Confirm only the expected part of the DOM updates.
  5. If the page has several ng-app roots, confirm you selected the intended one.
  6. 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

UseBehavior
ng-initEvaluates an AngularTS expression once during compilation. Use it for JSON objects, numbers, booleans, and other initial values.
ng-setterCopies 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

TaskBinding
Replace textng-bind or {{ expression }}
Add and remove classesng-class
Change inline stylesng-style
Hide while keeping the DOM aliveng-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:

  1. Listen for the form’s submit event.
  2. Prevent the browser from navigating away.
  3. Collect the current form controls.
  4. Send the request through $http.
  5. 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.

Route after a successful form

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:

StrategyUse it when
cache-firstData is effectively immutable and startup speed matters most.
network-firstFresh data matters, but cached data is a useful offline fallback.
stale-while-revalidateShowing 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 questionAngularTS mechanism
Trigger a requestng-get, ng-post, ng-put, ng-delete, data-trigger
Choose the destinationdata-target
Choose insertion behaviorswap
Render a server fragmentReturn HTML; AngularTS inserts and compiles it
Read JSONAssign $res in on-success or on-error
Show request stateloading, loading-class, bindings
Navigate after successdata-state-success
Prefetch from intentEvent trigger plus hidden scope state
Lazy load near the viewportng-viewport plus an HTTP directive
Keep local interaction stateScope, 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

SituationResponse contract
Saved data is needed locally2xx application/json with the canonical saved record
Only success matters204 No Content
Server owns the replacement UI2xx text/html with one fragment
User can correct submitted values422 application/json keyed by field name
Authentication expired401 application/json with a safe user message
User lacks permission403 application/json
Submitted version is stale409 application/json with conflict details
Server failed unexpectedly5xx 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

SurfaceShipping check
AuthorizationServer verifies this user may act on this resource
Cookie requestServer validates CSRF protection on every state-changing method
Bearer credentialToken never appears in URL, HTML, storage logs, or error output
HTML responseServer escapes untrusted values before AngularTS compiles it
JSON errorBody contains safe field messages, not stack or database details
CachePrivate entries are partitioned and cleared on logout
UploadServer checks size, MIME signature, filename, and storage limits
RedirectDestination 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.

Start with the browser’s form behavior

<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 observeFirst response
Fast typing starts a request per keyAdd a short delay after input settles
Scroll or resize fires continuouslyThrottle the event
The same read repeats unchangedReuse an appropriate public or user-partitioned cache
An old search overwrites a newer oneReject stale results or cancel the old request
Hover prefetch repeatsGuard it with loaded and loading state
Double-click repeats a writeDisable 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:

StepQuestion
PageDoes the useful server-rendered page work before enhancement?
InputAre labels, names, constraints, and server validation aligned?
RequestIs the method correct and sent only once?
AccessDoes the server check authentication, authorization, and CSRF?
ResponseAre status, content type, and body appropriate?
StateDoes success update only the intended scope and DOM?
FailureCan the user understand, retry, cancel, or recover safely?
ConcurrencyCan stale or duplicate requests corrupt state?
AccessibilityAre status and focus understandable without a pointer?
OperationsCan 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.

Submit the operation key with the form

<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.

Enhance a normal step form

<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.

Start with a real GET form

<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.

Submit logout as a protected form

<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.

Configure the server, not the browser workaround

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.

Let HTTP perform revalidation

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.

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.

What you needRecipe
Inspect a running pageInspect the application
Find an element’s scopeSelect elements through the scope
Include initial server dataPass data from the server
Change element content or classesUpdate the DOM
Replace an elementReplace DOM
Render server HTML in a regionSwap server HTML
React to stateRun effects
Handle browser and scope eventsHandle events
Submit user inputBuild forms
Call a JSON endpointUse REST services
Keep the page useful without JavaScriptEnhance progressively
Compare the server-driven approachUnderstand the HTMX similarity

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

ProblemRecipe
Submit controls and display responsesBuild forms
Render named server errorsShow server validation
Focus a rejected controlFocus the first error
Upload and cancel a fileUpload with cancellation
Search while typingBuild active search
Load one choice from anotherBuild dependent selects
Save several stepsBuild a multi-step form

Protect the write

ProblemRecipe
Protect a cookie-authenticated formSend CSRF proof
Prevent duplicate critical effectsUse an idempotency key
Confirm permanent deletionConfirm deletion
Preserve another person’s editHandle edit conflicts
Handle a uniqueness raceReturn uniqueness errors

Represent input correctly

ProblemRecipe
Submit currencySend money safely
Submit local timeSend dates with zones
Submit several recordsApply a bulk action
Build sign-in controlsSupport password managers
Harden the session formHarden session cookies

77 - Find an HTTP recipe

Choose statuses, bodies, caching, retries, fragments, and origin policy

Define the response first

ProblemRecipe
Choose status, body, and content typeDesign an HTTP response
Keep error bodies predictableUse an error envelope
Return a page or a fragmentNegotiate content explicitly
Swap trusted server markupReplace a region with server HTML

Control requests

ProblemRecipe
Recover from a failed readOffer manual retry
Stop duplicate or stale requestsControl repeated requests
Respect a server limitHandle rate limiting
Move slow work out of the requestRun a background job
Avoid retransferring unchanged dataRevalidate with ETag
Stream a generated file to diskUse a browser download

Handle deployment boundaries

ProblemRecipe
Call JSON servicesUse REST services
Serve a temporary outageReturn maintenance mode
Call another originConfigure cross-origin access

78 - Find a DOM recipe

Select, update, replace, animate, and focus application-owned elements

Wire elements to scope

ProblemRecipe
Expose an element without queryingSelect elements through scope
Change text and classesUpdate the DOM
Replace a nodeReplace DOM
Replace a region with server markupSwap server HTML

Build common interactions

ProblemRecipe
Edit one record in placeBuild inline editing
Remove a confirmed rowDelete a row
Update before a request completesUse an optimistic update
Load a server-rendered dialogOpen a server dialog

Keep updates usable

ProblemRecipe
Announce and focus request resultsMake requests accessible
Explain an empty collectionDesign an empty state
Keep focus after replacementPreserve focus after swaps

79 - Find a security recipe

Protect requests, sessions, redirects, content, credentials, and permissions

Protect server operations

ProblemRecipe
Audit a complete interactionRun the security audit
Protect cookie-authenticated writesSend CSRF proof
Enforce current permissionsHandle permission rejection
Prevent duplicate critical writesUse idempotency
End an authenticated sessionLog out safely

Protect browser content

ProblemRecipe
Validate a return destinationPrevent open redirects
Render user-authored formattingSanitize rich text
Restrict executable resourcesDeploy Content Security Policy
Restrict session deliveryHarden session cookies
Remove credentials from diagnosticsRedact secrets

Protect destructive actions

ProblemRecipe
Confirm permanent removalConfirm deletion
Prefer a reversible operationArchive and restore

80 - Find a performance recipe

Reduce requests, rendering, transfer, and main-thread work after measuring

Reduce network work

ProblemRecipe
Avoid repeated unchanged readsCache reference data
Use HTTP validatorsRevalidate with ETag
Delay, throttle, or cancel requestsControl repeated requests
Prefetch before a clickPrefetch a result

Reduce rendering work

ProblemRecipe
Page a large collectionPaginate on the server
Load more near the viewportBuild infinite scrolling
React to visibilityUse viewport observation
Keep simple reference dataUse caching

Move specialized work

ProblemRecipe
Receive incremental server changesUse server-sent events
Move CPU work off the main threadUse a worker
Run measured native computationUse WebAssembly
Move slow server work out of a requestRun a background job

Reduce shipped JavaScript

ProblemRecipe
The measured runtime contains unused featuresBuild a smaller production runtime

81 - Find a data correctness recipe

Preserve identity, precision, concurrency, validation, and audit history

Represent values correctly

ProblemRecipe
Keep public IDs unchangedUse opaque identifiers
Preserve currency precisionSubmit money safely
Preserve time meaningSend dates with zones
Seed initial server valuesPass server data

Protect concurrent changes

ProblemRecipe
Detect a stale editHandle edit conflicts
Handle a unique constraint raceReturn uniqueness errors
Make a write repeatableUse idempotency
Report partial batch resultsApply bulk actions

Preserve history and compatibility

ProblemRecipe
Keep useful cached recordsCache reference data
Keep state across an expired sessionHandle session expiration
Record a sensitive changeWrite an audit trail
Change a contract safelyUse a compatible rollout

82 - Find a navigation recipe

Keep URLs, server pages, router states, sessions, and redirects coherent

Start with server navigation

ProblemRecipe
Keep forms useful before enhancementUse progressive enhancement
Preserve search in the addressKeep filters in the URL
Preserve list navigationPaginate on the server
Return safely after sign-inValidate redirects

Add browser-owned state where needed

ProblemRecipe
Define application routesUse the router
Enter a state after sign-inHandle authentication
Enter the next saved form stepBuild a multi-step form
Recover from an expired sessionHandle session expiration
Enter a public state after logoutLog out safely

83 - Find an operations recipe

Trace failures, survive outages, control rollout, and explain sensitive changes

Diagnose a request

ProblemRecipe
Follow element, scope, request, and DOMDebug an HTTP interaction
Connect an error to server logsShow a request ID
Keep credentials out of telemetryRedact secrets
Check a whole featureRun the shipping check

Operate degraded systems

ProblemRecipe
Tell users when to retryHandle rate limits
Move long work to a worker processRun a background job
Serve a planned outageReturn maintenance mode

Change production safely

ProblemRecipe
Deliver a rollout decisionUse server feature flags
Explain a sensitive state changeRecord an audit trail
Support mixed release versionsRoll contracts out compatibly

84 - Find an advanced browser recipe

Use effects, viewport events, streams, workers, WebAssembly, and specialized UI

React to browser and scope changes

ProblemRecipe
Run cleanup-aware reactionsUse effects
Handle browser and scope eventsHandle events
React to element visibilityObserve the viewport
Stream server updatesUse server-sent events
Send and receive live messagesUse WebSocket

Add specialized execution

ProblemRecipe
Move computation off the main threadUse a worker
Call measured native computationUse WebAssembly
Remove unused framework codeBuild a smaller runtime
Cancel a large uploadControl an upload
Call an API on another originConfigure cross-origin access

Build specialized interaction

ProblemRecipe
Append results near the viewportBuild infinite scrolling
Update before the server respondsUse optimistic updates
Load a modal from the serverOpen a server dialog
Render sanitized authored contentHandle rich text

Integrate another framework

ProblemRecipe
Choose an integration boundaryChoose a bridge
Publish transient cross-root messagesUse the event bus
Read reflected web-component stateObserve Fluent UI attributes
Share current reactive stateUse an application model

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 boundaryUse
A transient message that something happenedThe event bus
State reflected by a custom element attributeng-observe
Current domain state needed by both sidesA 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.

Register one shopping-cart model

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

  1. Load the page with JavaScript disabled.
  2. Submit the form and confirm the server returns results.
  3. Enable JavaScript and confirm the live message appears.
  4. 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

  1. Click Add several times.
  2. Confirm the existing output node changes.
  3. 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

  1. Focus an input in one row.
  2. Refresh the collection with new objects carrying the same IDs.
  3. Confirm focus stays in that row.
  4. 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

  1. Open and close the panel repeatedly.
  2. Confirm ng-if runs component cleanup when the panel closes.
  3. Confirm hidden panels no longer issue requests or handle browser events.
  4. 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

  1. Delay short-query responses in the browser network tools.
  2. Search for a short term, then immediately search for a longer term.
  3. Confirm only the longer term changes the screen.
  4. 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

  1. Open and remove the component several times.
  2. Confirm only one timer or listener runs while it is visible.
  3. Confirm no callback runs after removal.
  4. 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.

  1. Reproduce the problem with realistic data and a production build.
  2. Open the browser Performance panel.
  3. Start recording immediately before the interaction.
  4. Stop after the screen reaches its final state.
  5. Find the longest main-thread task or network wait.
  6. Expand that task until you can name the function, layout, paint, or request responsible.
  7. 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:

  1. A delayed success shows loading, then results.
  2. An empty success shows the empty message.
  3. A failure shows retry and no loading indicator.
  4. 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.