NestJS walkthrough
better-push on NestJS, including the basePath trap.
See it running and inspect apps/demos/nestjs.
A NestJS API, from nothing to a notification you can see. Nest is the stack with one trap worth naming up front, so it gets said twice.
You need a Nest project with a DATABASE_URL.
1. Scaffold
npx @better-push/cli init --framework nestjsDetected NestJS · Prisma · package manager pnpm · source root src
Planned changes
create src/push.ts
create src/push.module.ts
create prisma/better-push.prisma
create prisma/better-push-indexes.sql
create prisma.config.ts
env .env.local (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT, …)init detects Nest even though @nestjs/platform-express depends on Express -
the more specific answer is the right one.
2. Install and import
pnpm add @better-push/core @prisma/client @prisma/adapter-pg pgimport { Module } from "@nestjs/common";
import { PushModule } from "./push.module";
@Module({ imports: [PushModule] })
export class AppModule {}That is the whole mount. The module routes through configure() middleware, so
there is no controller to write and no route decorator to keep in sync.
3. The basePath trap
Two places must agree, and both fail silently
forRootAsync requires an explicit basePath. The instance the factory
returns does not exist when Nest configures routing, so its own basePath
cannot be read. init writes "/api/push" in both files.
The module is middleware, not a controller, so app.setGlobalPrefix() is
not applied to it. If you set a global prefix, include it in basePath
here and in push.ts.
Neither mistake fails to compile. Both 404 every push endpoint at runtime, which is a confusing afternoon.
@Module({
imports: [
BetterPushModule.forRootAsync({
basePath: "/api/push", // must match push.ts, prefix included
useFactory: () => push,
}),
],
})
export class PushModule {}With app.setGlobalPrefix("v1"), both become "/v1/api/push".
4. Create the tables
npx @better-push/cli migratePrisma users: run the index SQL, or run migrate
prisma migrate builds a database missing all three of better-push's partial
indexes, because Prisma cannot express them and silently ignores what it
cannot express. One of them,
bp_digest_window_open_unique, carries behaviour: without it a burst of
twenty events flushes as twenty notifications instead of one digest.
better-push migrate creates everything. If you would rather Prisma owned the
tables, run prisma migrate dev and then
psql "$DATABASE_URL" -f prisma/better-push-indexes.sql - every time you reset
the database.
5. Wire your session
session: async (request) => {
const token = request.headers.get("authorization")?.replace("Bearer ", "");
const user = token ? await verifyJwt(token) : null;
return user ? { userId: user.sub } : null;
},The resolver takes a web-standard Request, not Nest's req - the bridge
converts one to the other, so nothing here depends on Nest's platform.
Need Nest DI inside the resolver? forRootAsync takes imports and inject
like any Nest async provider, so the factory can build the instance from
injected services.
6. Check it
npx @better-push/cli doctordoctor finds src/push.module.ts, confirms the schema, and - crucially for a
Prisma project - reports the three partial indexes as missing if you took the
prisma migrate path and skipped the SQL.
7. See a notification
import { emulator } from "@better-push/core/providers/emulator";
providers: [
webPush({ vapid: { /* ... */ } }),
...(process.env.NODE_ENV === "production" ? [] : [emulator()]),
],npx @better-push/cli devcurl -X POST http://localhost:3000/api/push/devices \
-H "authorization: Bearer $YOUR_TOKEN" \
-H "content-type: application/json" \
-d '{"platform":"android","provider":"emulator","token":"emulator-dev-1"}'await push.notify({ userId: "alice", title: "Hello", body: "It works." });It appears at http://127.0.0.1:4984. If nothing arrives, the basePath is the
first thing to check - see step 3.
More than one instance
Each forRoot/forRootAsync registration owns its own module class and mount,
so registering two instances at two paths works and neither overwrites the
other's routing.