@nestjs/serve-static puts a SPA behind a Nest app's router
Serve static websites (SPA's) using Nest framework (node.js) 🥦
At a glance
- What is it?
- @nestjs/serve-static is the official Nest package for serving single page applications from the same server as the API, with a fallback render path, optional route exclusion and per-request index.html transformation. It ships as ES modules only and needs a recent Node.js, and two of its options do not exist on the Fastify adapter.
- Who is it for?
- Adopt @nestjs/serve-static when you run a Nest application and a single page application behind one server and want the fallback index.html served from the framework rather than from a separate static host. Skip it if your project is CommonJS without the require(esm) path, if you need route exclusion on Fastify, or if you are serving plain MVC assets, where the README points at useStaticAssets() instead.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The package is for single page applications, not for asset folders
The scope of @nestjs/serve-static is narrow, and the README says so in one sentence. It is the package for serving static content like Single Page Applications from a Nest application. If you are building an MVC application, or you simply want to serve asset files such as images and documents, the documentation points you elsewhere, at the useStaticAssets() method, and links the Nest MVC technique page for the detail. The distinction is worth holding onto because the two do different jobs. Serving a SPA means that the client asks for a route that has no file behind it, and the server has to answer with the application shell so the client side router can take over. That fallback is the whole point of this package, and it is what renderPath controls. Serving assets means the client asks for a real file and the server finds it on disk. Nest can do both, but the SPA case is the one that needs its own package.
An ESM-only build is the install time decision
The installation step is short, and the requirement that follows it is not:
$ npm i --save @nestjs/serve-staticThis package ships as ES modules only and requires Node.js `^20.19.0 || ^22.12.0 || >=23.0.0`. The README then explains what that means for different projects. CommonJS applications can still consume it through Node's require(esm) support. TypeScript projects that compile to CommonJS need `"module": "nodenext"` in their tsconfig.json, because the older `"node16"` setting predates require(esm) and reports the error TS1479. The package manifest matches the documentation: the type field is module, the entry point is ./dist/index.js with types at ./dist/index.d.ts, and the exports map exposes the same pair. So the check you should run before upgrading anything is on the tsconfig side, not on the npm side. A project pinned to an older module setting will fail to build against this package even when its Node.js runtime is new enough.
forRoot() takes seven options and most defaults are sensible
The plugin is registered like any other Nest module, through ServeStaticModule.forRoot() with an options object. The documented properties are few and mostly have defaults that work. rootPath is the static files root directory and defaults to "client". serveRoot is the root path under which the static app is served and defaults to an empty string. renderPath is the path to render the static app, concatenated with the serveRoot value, and its default is a wildcard matching all paths. serveStaticOptions passes options straight through to the static file serving layer. useGlobalPrefix, false by default, prefixes the static app with the global prefix set through setGlobalPrefix(). exclude takes an array of path patterns or a single RegExp matched against the request path, so you can keep certain routes out of the static app. transformIndexHtml is the interesting one, a function of the form `(indexHtml: string, req) => string | Promise<string>` that rewrites the SPA fallback index.html before it is sent, which the README suggests for injecting request-specific variables.
Two options quietly disappear on the Fastify adapter
This is where a careful reading pays off. The renderPath option accepts a string or a RegExp, and the documentation notes that RegExp is not supported by @nestjs/platform-fastify. The exclude option carries a louder warning: it is not supported by fastify, and the README explains the workaround, which is to exclude routes using a regular expression by setting renderPath to one instead. So on Express you have two independent ways to carve paths out of the static app, and on Fastify you have one. That is a genuine functional difference between adapters rather than a documentation gap, and it will bite anyone who develops on Express with an exclude array and then deploys on Fastify. The peer dependency metadata reflects the same optionality. @nestjs/common and @nestjs/core are required, while express, fastify and @fastify/static are all marked optional, which lets an application use the package with whichever platform adapter it already has.
Registering the module against a client build
The usage example is short enough to read in full. You import ServeStaticModule, resolve the path to the built client with join, and pass it as rootPath:
import { Module } from '@nestjs/common';
import { join } from 'path';
import { ServeStaticModule } from '@nestjs/serve-static';
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: join(import.meta.dirname, '..', 'client')
})
]
})
export class ApplicationModule {}Two details are easy to miss. The path is built with import.meta.dirname, which is the ESM way to ask for the current directory, consistent with a package that ships as ES modules only. And the client path is resolved relative to the module file with '..' and 'client', which assumes the front end build lands in a client directory beside the compiled application, the default that rootPath also assumes. The README points to a full working example in the Nest repository under sample/24-serve-static, which is the reference to consult when the path resolution does not match your own build output layout.
transformIndexHtml is where a SPA gets request specific values
The fallback path is the mechanism that keeps a client side router working, and transformIndexHtml is the hook that makes it useful. The function receives the index.html contents and the request, and returns a string or a promise of one, and the README gives the reason directly, injecting request-specific variables. In a single page application served by the same server as the API, the shell often needs to know something about the request before the client code runs: a locale, a tenant identifier, a feature flag set, or a signed token. Hard coding those into the bundle at build time means one bundle per environment, while transforming the fallback per request keeps a single build and lets the server decide. The same hook can be used to inject a script tag or a meta tag. Because the function may be asynchronous, the response is not necessarily produced synchronously, which matters if you benchmark request handling under load. The README does not document caching of the transformed document, so treat the cost per request as your own design decision.
Version 12 on a Nest 12 peer range, and the tooling around it
The package version is 12.0.0 and it tracks the framework's major line, requiring @nestjs/common and @nestjs/core at ^12.0.0, with express at ^5.0.1 and fastify at ^5.2.1 as the optional platform peers. The release history shows a jump rather than a crawl: 5.0.4 in October 2025, 5.0.5 in April 2026 and 12.0.0 in August 2026, so upgrading from a 5.x installation is a major version change rather than a patch. Maintenance is current, with the last push on 2026-10-20 and the repository not archived. The repository layout is a modern Node package: TypeScript sources in lib/, end to end tests run by Vitest through the test:e2e script, oxlint for linting with a .oxlintrc.json, Prettier for formatting, commitlint for commit messages, husky for git hooks, renovate.json for dependency updates and release-it for publishing. The licence is MIT, and the wider Nest project is MIT licensed as well. That permissive licence is worth noting for anyone embedding it in a commercial product, since there is no copyleft obligation to pass on.
Editorial conclusion
Adopt @nestjs/serve-static when you run a Nest application and a single page application behind one server and want the fallback index.html served from the framework rather than from a separate static host. Skip it if your project is CommonJS without the require(esm) path, if you need route exclusion on Fastify, or if you are serving plain MVC assets, where the README points at useStaticAssets() instead. Before installing, check that your Node.js version satisfies ^20.19.0 || ^22.12.0 || >=23.0.0 and that your tsconfig.json uses "module": "nodenext".
Frequently asked questions
What is @nestjs/serve-static used for?
It is the Nest package for serving static content such as Single Page Applications from the same server as the application. If you are building an MVC application or only need to serve asset files such as images and documents, the README points to the useStaticAssets() method instead.
How do I install @nestjs/serve-static and what are the requirements?
Run `npm i --save @nestjs/serve-static`. The package ships as ES modules only and requires Node.js `^20.19.0 || ^22.12.0 || >=23.0.0`, and TypeScript projects compiling to CommonJS need `"module": "nodenext"` in their tsconfig.json.
Can I exclude certain routes from the static app?
Yes on Express, where the exclude option accepts an array of path patterns or a single RegExp matched against the request path. The exclude option is not supported by fastify, where the documented workaround is to set renderPath to a regular expression instead.
What does transformIndexHtml do?
It is a function of the form `(indexHtml: string, req) => string | Promise<string>` that transforms the SPA fallback index.html contents before it is sent to the client, for example to inject request-specific variables.
Which Nest and platform versions does the current release support?
Version 12.0.0 requires @nestjs/common and @nestjs/core at ^12.0.0, with express ^5.0.1, fastify ^5.2.1 and @fastify/static ^10.0.0 as optional peers. The last push to the repository was on 2026-10-20.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/nestjs-serve-static)Was this useful?