nestjs-ember-static
v0.3.1
Published
Nest - modern, fast, powerful node.js web framework (nestjs-ember-static)
Readme
Description
The nestjs-ember-static package enhances the Nest framework to serve static Single Page Applications (SPAs) built with Ember. Drawing inspiration from @nestjs/serve-static, this module stands out by offering tailored features for Ember applications.
Why choose this over @nestjs/serve-static? Primarily for its support of live reloading and configuration injection. Designed to facilitate the integration of EmberJS applications within a NestJS mono repository, this module ensures that both front-end and back-end can be accessed through the same port. During development, it preserves the Ember application's live reloading capabilities. Moreover, it provides a feature where the back-end can inject configuration metadata directly into the generated index.html. This functionality makes it ideal for developers seeking a seamless development experience with combined Ember and NestJS applications.
Compatibility
As of v0.3.0, this package supports NestJS 11 + Express 5 (and Fastify).
| nestjs-ember-static | NestJS | Express | path-to-regexp | Node |
| --------------------- | ------- | ------- | -------------- | ------- |
| >=0.3.0 | 11 | 5 | 8 | >=20 |
| <0.3.0 | 9–10 | 4 | 7 | >=18 |
Express and Fastify are declared as optional peer dependencies — install only
the adapter you use. NestJS 11's @nestjs/platform-express ships Express 5 by
default, so no extra pinning is required.
@nestjs/config is an optional peer dependency. The library no longer imports
ConfigModule itself, so it always uses your application's single copy. If your
Ember injection helpers or config modifiers inject ConfigService, register
ConfigModule globally so it is resolvable inside EmberModule:
ConfigModule.forRoot({ isGlobal: true });(Alternatively, pass your own imports to EmberModule.forRootAsync({ imports: [...] }).)
What changed in v0.3.0
This release deliberately decouples the library from the application’s Nest runtime.
That means the package no longer assumes it owns the app’s Nest instances for core
integration points like @nestjs/core or @nestjs/config.
In practical terms:
@nestjs/configis now fully owned by the host app. The library no longer importsConfigModuleon your behalf.- If your helpers need
ConfigService, registerConfigModuleglobally or pass it throughEmberModule.forRootAsync({ imports: [...] }). - The Ember loader setup now runs against the already-created Nest app, so the host application controls which Nest versions and adapters are in play.
If you are upgrading from an older version, the safest mental model is: the app
creates Nest, and nestjs-ember-static layers Ember behavior onto that app after
startup rather than trying to bootstrap Nest internals itself.
Note:
v0.3.0drops Express 4 support. If you still need Express 4 / NestJS 9–10, stay on the0.2.xline.
Installation
$ pnpm install nestjs-ember-staticUsage
To incorporate the EmberModule into your Nest application, configure it with
EmberModule.forRootAsync(...) and return one of these shapes:
- static mode:
{ metaTagName, static: { rootPath }, ... } - proxy mode:
{ metaTagName, proxy: { target }, ... }
The library currently uses a single async configuration path for real-world
setups. The forRoot() method exists, but does not accept options.
Static Configuration with forRootAsync
Use static.rootPath when Nest should serve a built Ember app from disk:
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { EmberModule, EmberModuleOptions } from 'nestjs-ember-static';
import { join } from 'path';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
EmberModule.forRootAsync({
useFactory: async (configService: ConfigService): Promise<EmberModuleOptions> => {
const env = configService.get('env', { infer: true });
return {
metaTagName: '@my-ember-app/config/environment',
static: {
rootPath:
env['name'] === 'development'
? join(env['appRoot'], '../frontend/dist')
: join(env['appRoot'], 'client'),
},
};
},
inject: [ConfigService],
}),
],
})
export class AppModule {}Proxy Configuration with forRootAsync
Use proxy.target during development when the Ember app is running separately:
import { Module } from '@nestjs/common';
import { EmberModule, EmberModuleOptions } from 'nestjs-ember-static';
@Module({
imports: [
EmberModule.forRootAsync({
useFactory: async (): Promise<EmberModuleOptions> => ({
metaTagName: '@my-ember-app/config/environment',
prefix: '/',
proxy: {
target: 'http://127.0.0.1:4200',
},
}),
}),
],
})
export class AppModule {}Additional Configuration Injection
For advanced configuration, such as dynamically setting properties or modifying the application title, you can add extra providers within the module. This enhances the module's flexibility and functionality, especially during development:
extraProviders: [
{
provide: EMBER_INJECTION_HELPERS,
useFactory: (configService: ConfigService) => [
new EmberConfigInjectionHelper('@my-ember-app/config/environment', configService),
new TitleInjectionHelper('Ember Application'),
],
inject: [ConfigService],
},
];By following these configurations, you can effectively integrate and serve your Ember SPA within a NestJS application, leveraging both static and dynamic setup options.
Inject Helpers
To implement custom inject helpers for dynamic configuration, you need to define them by extending an abstract class. They get passes the rootElement from the html document (see node-html-parser for how to mutate that).
export abstract class AbstractInjectionHelper {
public abstract process(htmlElement: HTMLElement): void;
}Here's an example of how you can create your own inject helpers:
export class TitleInjectionHelper extends AbstractInjectionHelper {
constructor(private title: string) {
super();
}
public process(htmlElement: HTMLElement): void {
htmlElement.querySelector('title')!.textContent = this.title;
}
}`Note: The InjectionHelpers given in this library are meant to be implemented by you. These are provided as examples to illustrate how you might structure your own helpers for configuration injection.
Adding Route Guards
The EmberModule supports route guards to control access to your Ember SPA.
To add a route guard, provide a guard into the EmberModule via the extraProviders:
import { Module } from '@nestjs/common';
import { join } from 'path';
import { EMBER_MODULE_GUARD, EmberModule } from 'nestjs-ember-static';
import { ConfigService } from '@nestjs/config';
import { MyCustomGuard } from './guards/my-custom.guard';
@Module({
imports: [
EmberModule.forRootAsync({
extraProviders: [
{
provide: EMBER_INJECTION_HELPERS,
useFactory: (configService: ConfigService) => [
new EmberClientConfigInjector('@3dlayermaker/device-controller-frontend/config/environment', configService),
],
inject: [ConfigService],
},
{
provide: EMBER_MODULE_GUARD,
useClass: ControllerApiAuthGuard,
},
],
}),
],
})
export class AppModule {}The guard will be applied to all routes handled by the EmberModule. The guard should implement the CanActivate interface, and you can control access by implementing the canActivate method.
Development mode
While developing, I typically run both the NestJS application and the EmberJS application simultaneously. My package.json includes a script like:
"start:dev": "concurrently --kill-others 'cd apps/frontend && pnpm run start' 'cd apps/backend && pnpm run start:debug'"In production, I build the Ember app and point static.rootPath at the built
output directory. In development, I usually run the Ember app separately and use
proxy.target so Nest forwards SPA requests to the running client.
Backing frameworks Express and Fastify
The nestjs-ember-static module supports both Express and Fastify as backing frameworks. This flexibility allows you to choose the framework that best suits your application's needs and preferences. Whether you are using the default Express or have switched to Fastify for improved performance, the nestjs-ember-static module integrates seamlessly with both, ensuring consistent functionality and easy setup.
However, it's important to note that Fastify has some limitations. For more details on these limitations, refer to the @nestjs/serve-static module documentation. This will help you understand any potential constraints and how they might impact your application when using Fastify as the backing framework.
Public Option Shape
The public configuration currently revolves around these top-level properties:
| Property | Type | Description |
| ------------- | -------- | ----------- |
| metaTagName | string | Required meta tag name used when injecting Ember config into index.html. |
| prefix | string | Optional mount prefix. Defaults to /. |
| beautify | object | Optional HTML beautify options for injected output. |
| static | object | Static serving mode. Use static.rootPath to point to built Ember assets. |
| proxy | object | Proxy mode. Use proxy.target to point at the running Ember dev server. |
static and proxy are mutually exclusive.
Migrating to v0.3.0 (NestJS 11 + Express 5)
v0.3.0 targets NestJS 11, whose @nestjs/platform-express is built on Express 5.
Express 5 uses path-to-regexp 8, which changes route-pattern syntax. The bare
* wildcard is no longer valid and must be written as a named wildcard:
*→*splat(unnamed segments are no longer allowed)- A catch-all render path is now
/{*splat}(or/*splat)
The library's default render path already uses the new syntax internally, so most applications need no changes.
If you are upgrading from an older release, note that the old top-level config shape shown in earlier docs is no longer accurate:
rootPathis now configured asstatic.rootPathproxy.targetis used for dev-server forwardingrenderPathis handled internally by the loaderliveReloadOriginis not part of the current public options interface
For full details see the Express 5 migration guide and the path-to-regexp 8 changes.
Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please read more here.
Stay in touch
- Author - Bas Kamer
License
As this is based on serve-static, this module is also licensed under the MIT license.
