Angular SPA
Drive client-side sub-route navigation inside an Angular page using @angular/router. AbsoluteJS forwards the request URL into renderApplication so the router resolves the correct initial route during SSR — and translates any router-issued redirect into an HTTP 302 automatically.
#How It Works
Angular's standalone-component model and provideRouter API make SPA setup minimal: export the routes, export providers = [provideRouter(routes)], place <router-outlet /> in the template, and the AbsoluteJS adapter handles the rest. There's no server-vs-client router-class swap (Angular Router uses the same class on both sides; the adapter selects the right location strategy under the hood).
#Wildcard Route
Refresh on any sub-route hits the server with the actual URL. Register a wildcard pattern so the same handler responds for every URL the page's router knows about:
// backend/server.ts — register a wildcard route per page so refresh /
// deep-link on any sub-route hits the same handler. The native router
// then dispatches based on the URL.
new Elysia()
.get('/dashboard', dashboardHandler)
.get('/dashboard/*', dashboardHandler)
.listen(3000);#Page Handler
Pass request through to the page handler. The Angular handler forwards request.url into renderApplication and installs the redirect bridge into the bootstrap providers — no other user wiring needed:
// backend/server.ts
import { handleAngularPageRequest } from '@absolutejs/absolute/angular';
import type * as AngularSpaPage from '../frontend/angular/pages/angular-spa';
const angularHandler = ({ request }: { request: Request }) =>
handleAngularPageRequest<typeof AngularSpaPage>({
headTag: generateHeadElement({
cssPath: asset(manifest, 'SpaCSS'),
title: 'AbsoluteJS SPA — Angular',
}),
indexPath: asset(manifest, 'AngularSpaIndex'),
pagePath: asset(manifest, 'AngularSpa'),
request, // ← request.url forwarded into renderApplication
});
new Elysia()
.get('/angular', angularHandler)
.get('/angular/*', angularHandler);#Page Component
The page module exports its root component as the default export, plus a providers array containing provideRouter(routes):
// frontend/angular/pages/angular-spa.ts
import { CommonModule } from '@angular/common';
import { Component, inject, signal } from '@angular/core';
import {
provideRouter,
Router,
RouterLink,
RouterLinkActive,
RouterOutlet,
type Routes,
} from '@angular/router';
@Component({
imports: [CommonModule],
selector: 'spa-home',
standalone: true,
template: '<h2>Home</h2>',
})
export class HomeView {}
@Component({
imports: [CommonModule],
selector: 'spa-settings',
standalone: true,
template: '<h2>Settings</h2>',
})
export class SettingsView {}
const routes: Routes = [
{ component: HomeView, path: 'angular' },
{ component: SettingsView, path: 'angular/settings' },
];
@Component({
imports: [CommonModule, RouterLink, RouterLinkActive, RouterOutlet],
selector: 'angular-spa-page',
standalone: true,
template: `
<nav>
<a routerLink="/angular" routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }">Home</a>
<a routerLink="/angular/settings" routerLinkActive="active">Settings</a>
</nav>
<router-outlet />
`,
})
export class AngularSpaComponent {
private router = inject(Router);
}
// Page-level providers — exported so the AbsoluteJS Angular handler
// installs them when bootstrapping. provideRouter is the standard
// Angular way to wire @angular/router; the AbsoluteJS adapter forwards
// request.url into renderApplication so the router resolves the correct
// initial route during SSR.
export const providers = [provideRouter(routes)];
export default AngularSpaComponent;<router-outlet />).provideRouter(routes). The Angular handler installs these when bootstrapping the app.Primitives:
@angular/router. Returns a provider that the AbsoluteJS handler installs at bootstrap. Export it as providers from your page module.<router-outlet /> anywhere in the page template.routerLinkActive applies a class when its route matches; combine with [routerLinkActiveOptions]="{ exact: true }" for exact-match links.inject(Router) at field-initializer time gives you the router instance — router.navigate(['/x']), router.events.subscribe(...), router.url, etc.{ path, component, canActivate?, data? } entries passed to provideRouter.#Redirects
The redirect bridge is wired automatically — no opt-in required. Angular Router redirects (from guards returning a UrlTree, or from redirectTo on a route) become HTTP 302s during SSR:
// AbsoluteJS automatically translates Angular Router redirects (issued
// from guards via router.navigate, redirectTo on a Route, etc.) into
// HTTP 302s during SSR. The bridge subscribes to router.events,
// watches for NavigationCancel with code Redirect, captures the
// next NavigationStart's URL, and writes Location + 302 to the
// outgoing response. No user code required — it's wired in
// handleAngularPageRequest itself.
// Example: a CanActivate guard returning a UrlTree triggers a 302.
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
export const requireAuth: CanActivateFn = () => {
const router = inject(Router);
if (!isLoggedIn()) return router.parseUrl('/login');
return true;
};
const routes: Routes = [
{ component: ProfileView, path: 'profile', canActivate: [requireAuth] },
];
// SSR request to /profile while logged out → 302 Location: /loginNavigationStart for the redirect target.responseInit.status = 302 and Location on the outbound response.