We are excited to announce the release of Analog 2.7! This release introduces Server Functions, typed and validated server operations you call like regular functions, along with experimental progressive Streaming SSR, agent-ready project scaffolding with a conformant Agent Plugin, and new strictness options for testing. Let's dive in.
Server Functions 🚀
The headline feature of Analog 2.7 is Server Functions: typed, validated operations that run on the server and are called from anywhere in your app (a component, an effect, a route resolver) with full type inference on both ends. They are defined in a .server.ts file, so their code and dependencies never reach the browser bundle.
Where load fetches data for one page and action handles one form post, a server function is an arbitrary operation you can call by name.
Defining a Server Function
Server functions are defined with serverFn from @analogjs/router/server in any .server.ts file under src:
// src/app/server-fns/products.server.ts
import { serverFn } from '@analogjs/router/server';
import { inject } from '@angular/core';
import * as v from 'valibot';
import { CatalogService } from './catalog.service';
// A handler alone is an input-less read, served over GET.
export const getProducts = serverFn(async () => {
return inject(CatalogService).list();
});
// A validation schema in front of the handler declares validated input, served over POST.
export const getProduct = serverFn(
v.object({ id: v.string() }),
async (input) => {
return inject(CatalogService).find(input.id);
},
);
The input schema is any Standard Schema validator: valibot, zod, and arktype all conform. It runs on the server before the handler, and invalid input is rejected without the handler ever running.
Notice the inject() call at the top of the handler. Server function handlers run inside the same request injector Analog builds for server-side rendering, bootstrapped from your app.config.server.ts, so providedIn: 'root' services just work, and request tokens like REQUEST, RESPONSE, and BASE_URL are available with no extra registration.
Calling a Server Function
Import the same exported function in a component and read it with injectServerFn, which returns an Angular resource:
// src/app/pages/products.page.ts
import { Component, input } from '@angular/core';
import { injectServerFn } from '@analogjs/router';
import { getProduct } from '../server-fns/products.server';
@Component({
template: `
@if (product.value(); as p) {
<h2>{{ p.name }}</h2>
} @else if (product.error()) {
<p>Could not load this product.</p>
} @else {
<p>Loading…</p>
}
`,
})
export default class ProductCard {
id = input.required<string>();
protected product = injectServerFn(getProduct, () => ({ id: this.id() }));
}
The args factory is reactive: the resource refetches whenever a signal it reads changes. For writes, injectServerFnMutation returns a callable you can await:
import { injectServerFnMutation } from '@analogjs/router';
export default class Checkout {
private placeOrder = injectServerFnMutation(placeOrderFn);
async submit(sku: string, qty: number) {
const { orderId } = await this.placeOrder({ sku, qty });
}
}
Both helpers dispatch through HttpClient, so your existing HttpInterceptorFns apply and HttpTestingController works in tests. During server-side rendering, calls skip HTTP entirely and run in-process, and a read resolved on the server is transferred to the client so the browser doesn't refetch on hydration.
Interceptors
Server-side interceptors are functional, provided through DI, and apply to every server function, the same model as HttpInterceptorFn. Use them for authentication, tenancy, and logging:
// src/app/server-fns/auth.interceptor.ts
import type { ServerFnInterceptorFn } from '@analogjs/router/server';
import { fail } from '@analogjs/router/server/actions';
import { inject } from '@angular/core';
export const authInterceptor: ServerFnInterceptorFn = (ctx, next) => {
const session = inject(SessionService);
if (!session.user()) {
return fail(401, { message: 'unauthenticated' });
}
return next(ctx.with({ user: session.user() }));
};
Register them in your server config with provideServerFns:
// src/app/app.config.server.ts
import { mergeApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import {
provideServerFns,
withServerFnInterceptors,
} from '@analogjs/router/server';
import { appConfig } from './app.config';
import { authInterceptor } from './server-fns/auth.interceptor';
export const config = mergeApplicationConfig(appConfig, {
providers: [
provideServerRendering(),
provideServerFns(withServerFnInterceptors([authInterceptor])),
],
});
Security by default
Server functions are HTTP endpoints, and Analog treats them that way. Route ids are derived at build time from the file and export name and served from an opaque /_analog/fn/<hash> route, so the endpoint surface can't be enumerated by guessing export names. Calls are same-origin by default, and cross-origin browser calls are rejected with a 403 before the function is even looked up. If you genuinely need cross-origin access, opt in explicitly with withAllowedOrigins([...]).
Server functions require Angular v19 or higher, as the client half is built on resource(). Check out the Server Functions docs for the full guide, including error handling with fail and redirect.
Streaming SSR (experimental) 🌊
Analog 2.7 introduces experimental support for progressive streaming server-side rendering, flushing the response to the browser as the app renders instead of buffering the whole document until it is complete.
The document head is sent immediately, each @defer (hydrate …) block is sent the moment it resolves on the server, and the authoritative document arrives last, so a slow block never holds back the rest of the page.
Enable the experimental.streaming option in your Vite config:
// vite.config.ts
import analog from '@analogjs/platform';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [analog({ experimental: { streaming: true } })],
});
Then use renderStream instead of render in main.server.ts:
// src/main.server.ts
import { renderStream } from '@analogjs/router/server';
import { config } from './app/app.config.server';
import { AppComponent } from './app/app.component';
export default renderStream(AppComponent, config);
Streaming builds on incremental hydration, so enable it in your client providers with provideClientHydration(withIncrementalHydration()). Content that should stream progressively goes in a @defer block with a hydrate trigger:
<h1>Dashboard</h1>
@defer (hydrate on immediate) {
<app-activity-feed />
} @placeholder {
<p>Loading activity…</p>
}
@defer (hydrate on viewport) {
<app-recommendations />
} @placeholder {
<p>Loading recommendations…</p>
}
A few details worth calling out:
- A title or meta set during render is applied to the streamed document once the render completes, before hydration runs.
- Search engine crawlers are served a buffered render with a fully resolved head instead of the streamed shell.
- Individual routes can fall back to buffered rendering with a
streaming: falseroute rule, the same wayssr: falsedisables SSR.
Streaming SSR is opt-in, requires Angular v21 or later, and the default buffered SSR path is unchanged. See the Streaming SSR docs for details.
Agent-Ready Projects 🤖
AI coding agents are part of many workflows now, and agents write better Analog code when they're given current framework conventions instead of guessing from training data. Analog 2.7 wires that context in from the start.
New projects scaffolded with create-analog and the Nx preset now include an AGENTS.md and a CLAUDE.md at the workspace root. Rather than freezing guidance into each scaffold, these are thin references pointing to a canonical AGENTS.md that ships inside @analogjs/platform, covering file-based routing, server/API routes, data fetching, and content loading. That way the guidance your agent reads always matches the version of Analog you have installed.
The @analogjs/platform package also now ships an Agent Plugins v1.0.0 conformant plugin: a plugin.json manifest and an Analog skill that expose the same guidance in a structured, machine-discoverable form for agent tooling that supports the spec.
Check out the AI integrations guide for how to point your tools at it.
Stricter Template Checking in Tests âś…
setupTestBed from @analogjs/vitest-angular picks up two new options that forward to TestBed.initTestEnvironment:
setupTestBed({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
By default, unknown elements and property bindings in templates only log warnings during tests. Opting in turns those NG8001/NG8002 diagnostics into thrown errors, so a typo'd selector fails the test instead of scrolling past in the console. Both options default to false, so existing test suites are unaffected.
Upgrading
To upgrade to Analog 2.7, run:
ng update @analogjs/platform@latest
If you're using Nx, run:
nx migrate @analogjs/platform@latest
For the full list of changes, see the changelog.
Partner with Analog 🤝
Continued development of Analog would not be possible without our partners and community. Thanks to our official deployment partner Zerops, code review partner CodeRabbit, and longtime supporters Snyder Technologies and Nx, and many other backers of the project.
Find out more information on our partnership opportunities or reach out directly to partnerships[at]analogjs.org.
Join the Community 🥇
- Visit and Star the GitHub Repo
- Join the Discord
- Follow us on Twitter
If you enjoyed this post, click the ❤️ so other people will see it. Follow AnalogJS and Brandon Roberts on Bluesky, and subscribe to my YouTube Channel for more content!
Top comments (0)