Back to Projects

ngx-libs-demo

A full-stack demo app that runs ngx-auth-client and ngx-api-client together against a real ASP.NET Core backend — sign-in, token refresh, role guards, retries and API errors.

2026

Duration

Oct 2026

Contributors

1 contributor

Technologies

9+ tech

N

ngx-libs-demo

ngx-libs-demo is a full-stack application built to test my two Angular libraries together against a real backend. ngx-auth-client owns the session — sign-in, token refresh, route guards, role checks and the bearer token. ngx-api-client owns every other API call — versioned URLs, retries with backoff, problem+json errors, success toasts and loading state.

bash
cp .env.example .env     # then set JWT_SIGNING_KEY: openssl rand -base64 48
docker compose up --build

Open http://localhost:8080 and sign in as alice (role user) or admin (roles user and admin).

Why build a demo

Both libraries have unit tests and a CI matrix that checks every supported Angular version against the packaged tarball. None of that answers the question a user actually has: does this work with my backend?

Libraries fail at their boundaries — where their assumptions meet a server, a cookie policy, or another library's interceptor. The only way to test those boundaries is to build something real on top of them, with a backend that has its own opinions. So I picked one with plenty: ASP.NET Core, whose defaults for validation errors, claims and cookies are not the ones my libraries were written against.

What's in it

FolderStack
client/Angular 22 — zoneless, signals, Signal Forms — with Vitest and ESLint
server/ASP.NET Core 10 minimal API, ASP.NET Core Identity, EF Core, PostgreSQL 18, xUnit v3 + Testcontainers
rootdocker-compose.yml (PostgreSQL + API + nginx), CI, documentation

The app itself is deliberately ordinary: sign up and sign in, personal notes, a profile page, an admin page that lists users and revokes their sessions, and a Playground page that makes the API misbehave on purpose.

Architecture

Browser
   │  /            /api/*
   ▼               ▼
        nginx  ── one origin
   │               │
Angular build   ASP.NET Core API ──► PostgreSQL 18

nginx serves the Angular build and proxies /api to the API, so the app and the API share one origin. That single decision removes a whole category of problems: the refresh cookie can be SameSite=Strict, there is no CORS configuration, and nobody has to think about withCredentials. In development, ng serve plays nginx's role through a proxy config.

The server tells the client how to authenticate

Before Angular even starts, main.ts asks the API which auth provider is in use:

json
{ "auth": { "provider": "local", "local": { "endpoints": { "signIn": "/api/auth/login", "refresh": "…" } } } }

provideAuth() needs its adapter at bootstrap, so the client loads this runtime configuration first and then picks the adapter — withHttpAuth() for the local provider today, withKeycloak() once Keycloak is wired in. Switching identity provider becomes a server setting, and one client build serves every environment. If the configuration can't be loaded, the app shows a plain error instead of half-working.

Nothing else in the client imports an adapter. Pages ask AuthService about capabilities — canSignIn(), canRegister(), canLoadProfile() — rather than checking which provider is active, and feature endpoints on the server read sub and roles from a validated token without knowing who issued it. Notes are keyed by sub rather than a foreign key to Identity's user table, so the data stays valid when the issuer changes.

How the two libraries share a request

The integration between the libraries comes down to one array — the order of the HTTP interceptors:

ts
withInterceptors([
  apiErrorInterceptor,   // outermost: one ApiError per operation, reported once
  apiSuccessInterceptor, // success toast for mutating calls
  retryInterceptor,      // retries the raw HttpErrorResponse → must be inside apiErrorInterceptor
  authTokenInterceptor,  // innermost (ngx-auth-client): each retried attempt gets a fresh token
]);

A GET /api/v1/notes that hits a 503 flows like this: the error interceptor passes it to retry, retry sends attempt one through the auth interceptor with a bearer token, the API answers 503, retry waits and sends attempt two — and the auth interceptor refreshes the token first if it is close to expiry. The page receives its notes, or exactly one ApiError if every attempt failed.

A few more connection points:

  • URLs line up without configuration. provideApi({ baseUrl: '' }) produces relative URLs like /api/v1/notes, which is exactly what the auth interceptor's default allowlist matches.
  • Auth calls stay out of the chain. The auth adapter calls /api/auth/* directly, bypassing every interceptor, so sign-in and refresh are never retried, versioned or toasted.
  • A 401 means the session is gone. The app's error handler receives every ApiError; a 401 can only mean the session couldn't be refreshed, so it sends the user to sign in.
  • No empty catch blocks. An error the handler has already shown still rejects the caller's promise, and a global handler ignores those — so pages simply await their calls and only catch when they display the error themselves, like a form showing field errors.

A real authentication backend

The local provider is a complete authentication server, not a stub that returns a fixed token:

  • Access token — a short-lived JWT (5 minutes by default) carrying sub, roles and standard claims, held only in the client's memory. roles is always an array and re-read on every refresh, so a role change reaches the client within one token lifetime.
  • Refresh token — 32 random bytes in an HttpOnly, Secure, SameSite=Strict cookie, scoped to /api/auth so it never travels with ordinary API calls. Only its SHA-256 hash is stored.
  • Rotation with reuse detection — every refresh issues a new token. Presenting an already-rotated one is treated as theft and revokes the whole session — except within a 10-second grace window from the same client, so two tabs reloading at once don't sign each other out.
  • Abuse limits — sign-in and sign-up are rate limited per IP and answer 429 with Retry-After; repeated wrong passwords lock the account for a few minutes; and sign-in failures return one message whatever the cause.

The admin page can revoke all of a user's refresh tokens, which makes "remote sign-out" something you can watch happen: the next background refresh in that user's tab fails, and the session ends.

A backend contract both libraries can rely on

Every error under /api is application/problem+json in the shape ngx-api-client reads, with a machine-readable code derived from the status (VALIDATION_ERROR, RATE_LIMITED, ENTITY_NOT_FOUND…) and a traceId. Feature endpoints are versioned in the URL, and paged endpoints return the shape getPage() expects. The server's integration tests check every point of that contract, against a real PostgreSQL started by Testcontainers.

The Playground

Most bugs in retry and error handling only appear when the server misbehaves, and real servers rarely misbehave on demand. The Playground page does:

EndpointBehaviour
/playground/flakyFails a chosen number of times with a chosen status, then succeeds — 429 adds Retry-After
/playground/status/{code}Any status as problem+json, including field errors on 400 and a custom code on 409
/playground/slowResponds after a chosen delay
/api/v1/… vs /api/v2/…The same resource with a different shape per version

One click triggers each scenario, and a written test plan covers the rest: session restore, background refresh, guards and roles, remote sign-out, retries, error normalisation, versioning and the open-redirect check on the login page.

What it found

The demo's most useful output is a document: library feedback, recording what went wrong while integrating the libraries.

  • ngx-api-client 0.3.0 — the provideApi() JSDoc example ordered the interceptors so that retries never run, contradicting the README; and ApiError.errors was copied from the response without checking its shape, so ASP.NET Core's validation dictionary arrived where an array was promised. The demo works around both — a test pins the interceptor order, and the server flattens validation errors — while the fixes belong in the library.
  • ngx-auth-client 0.2.0 — no issues: the defaults matched an ASP.NET Core backend once the server followed the documented contract. One documentation note: the bearer allowlist only matches relative URLs, so an absolute baseUrl in ngx-api-client would silently send requests without a token. A cross-reference between the two READMEs would prevent that.

None of these would have shown up in either library's own tests, because each one lives at a boundary — between a library and a server, or between the two libraries.

Code conventions

  • Standalone components, OnPush, signals for state and computed() for anything derived.
  • Each feature that calls the API has its own *-api.ts service built on ApiService.
  • Forms use Signal Forms, and server field errors from ApiError.errors are routed to the matching fields.
  • On the server, Program.cs is composition only, each feature has its own folder, and only the Auth/ folder knows how tokens are issued or validated.

What's next

Keycloak is designed but not yet wired: the settings, client configuration and implementation plan are in place, and selecting it today stops the app at startup with a clear error rather than running half-configured. Once it lands, the same client build will run against either provider — the strongest possible test of ngx-auth-client's provider-agnostic port.

ngx-libs-demo

A full-stack application for testing two Angular libraries together against a real backend:

Library Role in the app
@ismailza/ngx-auth-client The session: sign-in, token refresh, authGuard, role checks, the bearer token. The adapter (withHttpAuth() today) follows the server's configuration.
@ismailza/ngx-api-client Every other API call: versioned URLs, retry with backoff, problem+json errors as ApiError, success toasts, loading state.
Folder Stack
server/ ASP.NET Core 10 minimal API, ASP.NET Core Identity, EF Core, Postgres 18, xUnit v3 + Testcontainers
client/ Angular 22 (zoneless, signals, Signal Forms), Vitest, ESLint
root docker-compose.yml (Postgres + API + nginx), CI, docs

Quick start

cp .env.example .env     # then set JWT_SIGNING_KEY: openssl rand -base64 48
docker compose up --build

Open http://localhost:8080 and sign in as alice (role user) or admin (roles user, admin). Both use the password Passw0rd!.

To work on the code with hot reload, see docs/development.md.

What to try

docs/test-scenarios.md is a checklist covering both libraries: session restore, background refresh, guards and roles, remote sign-out, retries, error normalisation, versioning and the open-redirect check. The Playground page triggers most of the API scenarios with one click.

Documentation

Document Contents
Architecture How a request flows, the interceptor chain, project layout, design choices
Auth providers How the server picks the provider and the client follows; the Keycloak plan
Backend contract Endpoints, tokens and cookie, error format, paging, versioning
Development Running locally, tests, lint, migrations, configuration reference
Test scenarios Manual test plan for both libraries
Library feedback Issues found in the libraries while building this demo

More Projects

ngx-auth-client
2026Current
A published npm package that exposes Angular authentication as signals instead of getters, ships Keycloak and HTTP adapters behind a provider-agnostic port, and keeps tokens out of localStorage entirely.
AngularTypeScriptKeycloak+7
ngx-api-client
2026Current
A published npm package extracted from the same api.service.ts I had copied across four Angular projects — configurable versioning, RFC 9457 error normalization, retry with backoff, and zero UI dependencies.
AngularTypeScriptRxJS+5