better-push
Integrate your stack

NestJS

Mount better-push as a Nest module, with forRoot and forRootAsync.

src/app.module.ts
import { Module } from "@nestjs/common";
import { BetterPushModule } from "@better-push/core/nestjs";
import { push } from "./push";

@Module({
  imports: [BetterPushModule.forRoot({ push, basePath: "/api/push" })],
})
export class AppModule {}
src/main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

const app = await NestFactory.create(AppModule);
await app.listen(3000);

How it mounts

The module routes through NestModule.configure() middleware, not a controller. A controller would need a decorated class with a route per endpoint, which would drag experimentalDecorators and emitDecoratorMetadata into better-push's own tsconfig - changing how the entire package compiles, for every app that never touches Nest.

The middleware is applied to every route and decides for itself whether a path belongs to better-push, forwarding everything else with next(). Nest's wildcard route syntax changed between major versions (path-to-regexp v6 to v8); a prefix check works on both, and is the same check the router would make.

@nestjs/common and @nestjs/core are optional peer dependencies and the entry imports neither. Everything Nest hands it is typed structurally.

forRootAsync

When the instance depends on something Nest owns - a config service, a database connection - build it in a factory:

src/app.module.ts
@Module({
  imports: [
    ConfigModule,
    BetterPushModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      basePath: "/api/push",
      useFactory: (config: ConfigService) =>
        betterPush({
          database: postgresAdapter(config.getOrThrow("DATABASE_URL")),
          providers: [webPush({ vapid: config.getOrThrow("VAPID") })],
          session: betterAuthSession(auth),
          basePath: "/api/push",
        }),
    }),
  ],
})
export class AppModule {}

basePath is required here and must match the one in the config: the instance the factory returns does not exist when routing is set up, so its own basePath cannot be read. The module says so rather than guessing.

The resolved instance is also provided under the BETTER_PUSH token, so the rest of your app can inject it:

import { BETTER_PUSH } from "@better-push/core/nestjs";

constructor(@Inject(BETTER_PUSH) private readonly push: BetterPush) {}

Caveats

Global prefix. app.setGlobalPrefix("api") applies to controllers, and this is middleware. Include the prefix in basePath yourself:

app.setGlobalPrefix("api");
BetterPushModule.forRoot({ push, basePath: "/api/push" });

Body parser. NestFactory.create(AppModule, { bodyParser: false }) is recommended but not required - the bridge re-serializes a body a parser has already consumed. With the parser on, malformed JSON is rejected by Nest before better-push sees it, so that one case answers with Nest's error rather than better-push's.

Fastify is untested. The middleware path is exercised on the Express platform. FastifyRequest.raw/FastifyReply.raw are the objects it would need, and nothing here has been run against them - so it is documented as untested rather than claimed as supported.

Two instances in one process are fine: each registration gets its own module class closing over its own mount, so a second forRoot cannot redirect the first.

On this page