All authors

Claude Skills by HoangNguyen0403
github.com/HoangNguyen04031,434 skills30 installs1,975 views
- Laravel Sessions MiddlewareRegenerate the session immediately after successful authentication: ```php if (Auth::attempt($credentials)) { $request->session()->regenerate(); // Prevents session fixation return redirect()->intended('/dashboard'); } ``` On logout: ```php $request->session()->invalidate(); $request->session()->regenerateToken(); ``` For scaled production environments, prefer Redis: ```env SESSION_DRIVER=redis ``` Install `predis/predis`, use `config('session.driver')` rather than `env()` in application code...Votes: 0GitHub stars: 549
- Laravel Sessions MiddlewareAssuming `SecurityHeaders` exists at `app/Http/Middleware/SecurityHeaders.php`, register it globally in `bootstrap/app.php`: ```php <?php use App\Http\Middleware\SecurityHeaders; use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void {...Votes: 0GitHub stars: 549
- Laravel Sessions MiddlewareFix Laravel session configuration by ensuring it is read through `config()` and clearing cached configuration. 1. In `.env`: ```env SESSION_DRIVER=redis ``` Install the Redis client if needed: ```bash composer require predis/predis ``` 2. In `config/session.php`: ```php 'driver' => env('SESSION_DRIVER', 'file'), ``` Do not call `env('SESSION_DRIVER')` in application code. Use: ```php $driver = config('session.driver'); ``` 3. Clear and rebuild the configuration cache: ```bash php artisan conf...Votes: 0GitHub stars: 549
- Laravel TestingCreate a factory and a Pest feature test that exercises the HTTP boundary: ```bash php artisan make:factory PostFactory --model=Post ``` ```php use Illuminate\Foundation\Testing\RefreshDatabase; uses(RefreshDatabase::class); it('creates a post', function () { $response = $this->postJson('/api/posts', [ 'title' => 'Example', 'body' => 'Content', ]); $response->assertStatus(201)->assertJsonStructure(['data' => ['id', 'title']]); }); ``` Use factories for setup, isolate database state with `Refr...Votes: 0GitHub stars: 549
- Laravel TestingApply `RefreshDatabase` to the Pest file or shared test setup: ```php use Illuminate\Foundation\Testing\RefreshDatabase; uses(RefreshDatabase::class); it('stores a post', function () { Post::factory()->create(); expect(Post::count())->toBe(1); }); ``` Laravel migrates the test database and resets it between tests, preventing state leakage. Use `DatabaseTransactions` when transaction rollback is sufficient and faster, but choose `RefreshDatabase` for tests that need a clean schema/database lif...Votes: 0GitHub stars: 549
- Laravel TestingGenerate and define an Eloquent factory: ```bash php artisan make:factory PostFactory --model=Post ``` ```php public function definition(): array { return [ 'title' => fake()->sentence(), 'body' => fake()->paragraph(), 'published_at' => fake()->optional()->dateTime(), ]; } ``` Use it in tests with `Post::factory()->count(3)->create(['user_id' => $user->id])`. Factories provide valid, readable test data and keep persistence setup consistent. Prefer them over `DB::table()->insert()`; use states...Votes: 0GitHub stars: 549
- Laravel TestingMock the external service at the container boundary: ```php $payment = $this->mock(PaymentService::class); $payment->shouldReceive('charge') ->once() ->with(100) ->andReturnTrue(); $response = $this->post('/checkout', ['amount' => 100]); $response->assertSuccessful(); ``` Use `$this->spy()` when you need behavior verification without strict interaction expectations. Tests must not make real network calls: bind a mock, fake the transport, or use a local stub. Assert both the HTTP outcome and t...Votes: 0GitHub stars: 549
- Laravel TestingAssert the status, JSON payload, structure, and important headers explicitly: ```php $response = $this->postJson('/api/posts', ['title' => 'Hello']); $response ->assertStatus(201) ->assertHeader('Content-Type', 'application/json') ->assertJson(['data' => ['title' => 'Hello']]) ->assertJsonStructure(['data' => ['id', 'title', 'created_at']]); ``` Use `assertJsonValidationErrors()` for invalid input and assert `422` rather than only checking that the request failed. Keep assertions tied to the ...Votes: 0GitHub stars: 549
- Laravel TestingConfigure the test environment to use SQLite in memory: ```xml <env name="DB_CONNECTION" value="sqlite"/> <env name="DB_DATABASE" value=":memory:"/> ``` Keep `RefreshDatabase` on database-backed tests so migrations and state reset are deterministic. Run the suite with the project’s normal Pest/PHPUnit command and ensure migrations use SQLite-compatible features; if production-specific SQL cannot run on SQLite, use a dedicated integration database for that case. Use Eloquent factories instead ...Votes: 0GitHub stars: 549
- Laravel ToolingCreate the command with Artisan's generator: ```bash php artisan make:command SendNewsletters ``` Define an explicit signature and return an integer from `handle()`: ```php protected $signature = 'newsletters:send {--queue}'; public function handle(): int { // coordinate the use case or dispatch a job return self::SUCCESS; } ``` Commands under `app/Console/Commands/` are auto-discovered. Keep heavy work in queued Jobs and use the command as an operator-facing entry point. Schedule it in the c...Votes: 0GitHub stars: 549
- Laravel ToolingUse Vite with the Laravel plugin and load the entry points through Blade: ```bash npm install ``` ```js // vite.config.js import laravel from 'laravel-vite-plugin'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [laravel(['resources/css/app.css', 'resources/js/app.js'])], }); ``` ```blade @vite(['resources/css/app.css', 'resources/js/app.js']) ``` Use `npm run dev` for HMR and `npm run build` for production. Vite handles asset versioning; do not add manual cache-b...Votes: 0GitHub stars: 549
- Laravel ToolingInstall Pint as a development dependency and run it against the project: ```bash composer require laravel/pint --dev ./vendor/bin/pint ``` If the project uses `pint.json`, keep its preset and exclusions under version control; the Laravel preset should be the baseline. Run Pint in CI and before merging so formatting drift is caught consistently. Treat Pint as an automated style check/formatter, not as a substitute for tests or architectural review.Votes: 0GitHub stars: 549
- Laravel ToolingInstall and initialize Horizon, then configure its supervisors: ```bash composer require laravel/horizon php artisan horizon:install php artisan horizon ``` Define queue names, process counts, balancing, retries, and environment overrides in `config/horizon.php`. Protect `/horizon` with the authorization gate in `HorizonServiceProvider`; the dashboard must not be public by default. Horizon provides runtime visibility into throughput, latency, workers, and failed jobs, so use it for production...Votes: 0GitHub stars: 549
- Laravel ToolingMove from Mix to Vite by installing the Laravel Vite plugin, defining the entry points in `vite.config.js`, and replacing Blade's `mix()` calls with `@vite()`: ```blade @vite(['resources/css/app.css', 'resources/js/app.js']) ``` Remove `laravel-mix` and stale Mix configuration after the new build is verified. Use `npm run dev` for local HMR and `npm run build` for production. Put JavaScript in `resources/js` rather than embedding scripts in Blade templates. Vite owns the manifest and asset ve...Votes: 0GitHub stars: 549
- Laravel ToolingDefine CSS and JavaScript as Vite entry points and include them from the Blade layout: ```js // vite.config.js export default defineConfig({ plugins: [laravel(['resources/css/app.css', 'resources/js/app.js'])], }); ``` ```blade @vite(['resources/css/app.css', 'resources/js/app.js']) ``` Run `npm run dev` during development and `npm run build` for the production bundle. Keep asset code in `resources/`, use the Laravel plugin for integration, and avoid manual database or asset-manifest edits. V...Votes: 0GitHub stars: 549
- Nestjs Api StandardsUse dedicated response DTOs and apply a global interceptor that maps successful results to a stable envelope such as `{ statusCode, data, meta }`. Map entities to DTOs with `plainToInstance` so internal fields never leak, and document every endpoint with its exact `@ApiResponse({ status, type })`. Standardize errors with an `ApiErrorResponse` containing `statusCode`, `message`, `error`, `timestamp`, and `path`. ```ts @Injectable() export class TransformInterceptor<T> implements NestIntercepto...Votes: 0GitHub stars: 549
- Nestjs Api StandardsDecorate nested object or array properties with both `@ValidateNested()` and `@Type(() => NestedDto)`, then enable transformation and validation at the application boundary. ```ts export class AddressDto { @IsString() street!: string; } export class CreateUserDto { @ValidateNested() @Type(() => AddressDto) address!: AddressDto; @ValidateNested({ each: true }) @Type(() => AddressDto) addresses!: AddressDto[]; } app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true, forbidNon...Votes: 0GitHub stars: 549
- Nestjs Api StandardsUse strict page and response DTOs rather than returning an unbounded array. A typical contract is `PageOptionsDto` (`page`, `take`, `order`) plus `PageDto<T>` containing `data` and pagination metadata. Validate and cap `take`, apply deterministic ordering, and use a cursor strategy when offset pagination becomes expensive. ```ts export class PageOptionsDto { @Type(() => Number) @IsInt() @Min(1) page = 1; @Type(() => Number) @IsInt() @Min(1) @Max(100) take = 20; @IsEnum(Order) order: Order = O...Votes: 0GitHub stars: 549
- Nestjs ArchitectureOrganize the application by business capability. Each feature module should own its controllers, application services, and persistence adapters; keep cross-cutting configuration and database modules in a core area, and export only genuinely shared stateless helpers. Keep controllers thin, put orchestration in services/use cases, and register dependencies once in the module graph. Use singleton providers by default; choose request scope only for measured request-local state such as tenant cont...Votes: 0GitHub stars: 549
- Nestjs ArchitectureNo. A controller should parse transport input, invoke an application service, and shape the HTTP response. Business rules and orchestration belong in services or use cases so they remain reusable and testable outside HTTP. ```ts @Controller('orders') export class OrdersController { constructor(private readonly orders: OrdersService) {} @Post() create(@Body() dto: CreateOrderDto, @CurrentUser() user: User) { return this.orders.create(dto, user.id); } } ``` Keep providers singleton by default a...Votes: 0GitHub stars: 549
- Nestjs ArchitectureRegister the entity with the feature module and inject its repository through Nest's DI container: ```ts @Module({ imports: [TypeOrmModule.forFeature([UserEntity])], providers: [UsersService], }) export class UsersModule {} @Injectable() export class UsersService { constructor( @InjectRepository(UserEntity) private readonly users: Repository<UserEntity>, ) {} } ``` The root database module should use `TypeOrmModule.forRootAsync` and `ConfigService`; `synchronize` must be `false` in production...Votes: 0GitHub stars: 549
- Nestjs BullmqRegister the notifications queue with shared worker options and retention defaults, then add a processor that extends the project `BaseProcessor` rather than `WorkerHost`. ```ts @Processor(NOTIFICATION_QUEUE, { drainDelay: QUEUE_DRAIN_DELAY_MS, stalledInterval: QUEUE_STALLED_INTERVAL_MS, maxStalledCount: 1, }) export class DigestProcessor extends BaseProcessor { @Process(WEEKLY_DIGEST_JOB) async handle(job: Job<WeeklyDigestPayload>) { await this.notifications.sendWeeklyDigest(job.data); } } `...Votes: 0GitHub stars: 549
- Nestjs BullmqThe likely cause is idle BullMQ workers polling Redis too aggressively. A bare processor uses BullMQ's very small default `drainDelay`; multiplied by a continuously running dev worker, it can consume the Upstash free quota even when no jobs are available. Use local Docker Redis for development and configure every processor with shared constants such as `drainDelay: 10_000`, `stalledInterval: 60_000`, and a bounded `maxStalledCount`. Do not point dev workers at Upstash. Also remove completed/f...Votes: 0GitHub stars: 549
- Nestjs BullmqDo not let Redis failure from a global throttler or producer become an unhandled HTTP error. Make the throttler storage fail open: catch every Redis error and return a pass-through increment result, so a Redis outage does not reject all routes. For producers, persist the database record first and wrap `queue.add()` in `try/catch`; record or retry the enqueue failure separately. Configure processors with `drainDelay`, `stalledInterval`, and `maxStalledCount`, and extend the shared `BaseProcess...Votes: 0GitHub stars: 549
- Nestjs CachingUse an L1/L2 design: an in-process `lru-cache` for small, mostly static values and Redis as the shared L2 cache across pods. Configure Nest cache-manager with `cache-manager-redis-yet`, and use a custom key that canonicalizes query parameters. Stale-while-revalidate lets requests serve a still-valid stale value while one worker refreshes it. Add jitter to TTLs and use a lock/single-flight mechanism so only one process recomputes a missing or expired value. Memory-only storage is not sufficien...Votes: 0GitHub stars: 549
- Nestjs CachingGroup keys with Redis Sets rather than scanning the keyspace. When writing a cached entity, add its key to a tag set, for example `SADD tag:post:1 cache:post:1`. To invalidate the group, read the members, delete those keys, and delete the tag set. Avoid `KEYS`, which is O(N) and can block production Redis. For multi-pod correctness, use Redis rather than `MemoryStorage`, and add jitter to TTLs. Domain events such as `PostUpdated` can perform invalidation asynchronously; make the operation ide...Votes: 0GitHub stars: 549
- Nestjs CachingPrevent a stampede by combining three controls: add random TTL jitter so keys do not expire together, use a distributed lock so only one worker recomputes a value, and let other requests receive stale data or wait briefly while the refresh completes. For high-traffic reads, use L1 memory plus Redis L2 and a stale-while-revalidate flow. The lock must have an expiry and the recomputation must be idempotent; release it only if the caller owns it. Do not solve grouped invalidation with Redis `KEY...Votes: 0GitHub stars: 549
- Nestjs ConfigurationInitialize `@nestjs/config` globally and give it a startup validation schema. Required variables should make boot fail immediately instead of producing a partially configured service. ```ts ConfigModule.forRoot({ isGlobal: true, validationSchema: Joi.object({ NODE_ENV: Joi.string().valid('development', 'production').default('development'), PORT: Joi.number().default(3000), DATABASE_URL: Joi.string().required(), }), }); ``` Inject `ConfigService` into modules and services, preferably through a...Votes: 0GitHub stars: 549
- Nestjs ConfigurationUpdate the application validation model and every runtime contract, not just the local `.env` file: - add the variable and its type/required rule to `src/config/env.validation.ts` (or the Joi schema); - document a placeholder in `.env.example` and add appropriate development/test values; - map it explicitly in CI/CD and infrastructure, such as Kubernetes `ConfigMap`/`Secret`, ECS/Cloud Run settings, or pipeline variables; - consume it through `ConfigService` or a typed configuration layer. Th...Votes: 0GitHub stars: 549
- Nestjs ConfigurationAvoid direct `process.env` access in services. Load and validate variables once through `ConfigModule.forRoot`, then inject `ConfigService` or a typed configuration object: ```ts @Injectable() export class MailService { constructor(private readonly config: ConfigService) {} send() { const host = this.config.getOrThrow<string>('MAIL_HOST'); // use host... } } ``` This centralizes validation, avoids magic strings, supports typed getters, and makes dependencies testable. Required values should b...Votes: 0GitHub stars: 549
- Nestjs Controllers ServicesCreate a parameter decorator that types the request once instead of exposing `@Request() req` throughout controllers. ```ts export interface RequestWithUser extends Request { user: User; } export const CurrentUser = createParamDecorator( (_data: unknown, ctx: ExecutionContext): User => ctx.switchToHttp().getRequest<RequestWithUser>().user, ); ``` Use it as `create(@CurrentUser() user: User)`. The authentication guard remains responsible for populating `request.user`; the decorator should only...Votes: 0GitHub stars: 549
- Nestjs Controllers ServicesRegister one global `ValidationPipe` at bootstrap: ```ts app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true, })); ``` Use DTO classes with `class-validator` decorators. `transform` instantiates DTOs and converts primitives, `whitelist` strips properties without decorators, and `forbidNonWhitelisted` rejects unexpected input instead of silently accepting it. Apply `ParseIntPipe` or `ParseUUIDPipe` to route IDs, and use `@ValidateNested()` plus ...Votes: 0GitHub stars: 549
- Nestjs Controllers ServicesUse `OnApplicationShutdown` for cleanup that must happen when the application is shutting down, such as closing clients, consumers, or timers. Enable signal handling in bootstrap: ```ts @Injectable() export class QueueLifecycle implements OnApplicationShutdown { async onApplicationShutdown(signal?: string) { await this.consumer.close(); this.logger.log(`Closed on ${signal ?? 'application'} shutdown`); } } app.enableShutdownHooks(); ``` Use `OnModuleInit` for startup connection/setup work. Kee...Votes: 0GitHub stars: 549
- Nestjs DatabaseGenerate and review an explicit migration whenever an entity changes, with production `synchronize: false`. Run migrations from CI/CD or a Kubernetes init container before application pods start, not from `main.ts` in every replica. ```sh pnpm migration:generate -- -d dist/database/data-source.js src/migrations/AddIndex pnpm migration:run -- -d dist/database/data-source.js ``` Use production credentials/configuration in the migration job, inspect the generated SQL, and make destructive change...Votes: 0GitHub stars: 549
- Nestjs DatabaseHide TypeORM behind a repository adapter so services depend on a domain-facing interface rather than `Repository<Entity>` or raw SQL. Inject the TypeORM repository into the adapter through the module graph: ```ts @Module({ imports: [TypeOrmModule.forFeature([UserEntity])], providers: [UsersTypeOrmRepository, UsersService], exports: [UsersService], }) export class UsersModule {} @Injectable() export class UsersTypeOrmRepository { constructor(@InjectRepository(UserEntity) private readonly repo:...Votes: 0GitHub stars: 549
- Nestjs DatabaseUse a TypeORM `QueryRunner` or an injected `EntityManager` for every multi-step mutation, and pass that manager through all repository operations so they share one transaction. ```ts const runner = dataSource.createQueryRunner(); await runner.connect(); await runner.startTransaction(); try { const users = runner.manager.getRepository(UserEntity); await users.save(user); await runner.manager.getRepository(AuditEntity).save(audit); await runner.commitTransaction(); } catch (error) { await runne...Votes: 0GitHub stars: 549
- Nestjs DeploymentUse a multi-stage Dockerfile: install/build with development dependencies, then copy only compiled `dist` and production dependencies into a small runtime image. Run as the non-root `node` user. ```dockerfile FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build && npm prune --omit=dev FROM node:22-alpine WORKDIR /app COPY --from=build --chown=node:node /app/dist ./dist COPY --from=build --chown=node:node /app/node_modules ./node_modules COPY --...Votes: 0GitHub stars: 549
- Nestjs DeploymentSet V8 old-space to roughly 75–80% of the Kubernetes memory limit, leaving room for native memory and the runtime. For a 1 GiB limit, an initial value is about 800 MiB: ```yaml containers: - name: api resources: limits: { memory: 1Gi } command: ["node"] args: ["--max-old-space-size=800", "dist/main.js"] ``` Tune from heap/OOM metrics rather than assuming the limit equals usable V8 heap. Set requests appropriately, define liveness/readiness probes, and keep graceful shutdown enabled so pods dr...Votes: 0GitHub stars: 549
- Nestjs DeploymentRun TypeORM migrations once per release in a Kubernetes init container or a serialized pre-deploy/CI job. Do not run them in `main.ts`, because every application pod may race to apply the same migration. The migration job should use the production `DataSource`, wait for database connectivity, run `migration:run`, and fail the rollout on error. Keep `synchronize: false`, inspect generated migrations, and use expand-contract changes for zero-downtime compatibility. Init containers are a good fi...Votes: 0GitHub stars: 549
- Nestjs DocumentationEnable the Nest CLI Swagger plugin so DTO metadata is generated from TypeScript types and validation decorators instead of adding `@ApiProperty()` to every field. ```json { "compilerOptions": { "plugins": [{ "name": "@nestjs/swagger" }] } } ``` Still annotate controllers with `@ApiTags`, exact `@ApiResponse({ status, type })`, and auth decorators. Use named enums and explicit decorators where the plugin cannot infer a union, generic, or runtime-only shape. Disable `/docs` in production unless...Votes: 0GitHub stars: 549
- Nestjs DocumentationGenerics are erased at runtime, so register both the wrapper and item DTO with `ApiExtraModels`, then describe the array schema with `getSchemaPath`. ```ts export const ApiPaginatedResponse = <TModel extends Type<unknown>>(model: TModel) => applyDecorators( ApiExtraModels(PageDto, model), ApiResponse({ status: 200, schema: { allOf: [ { $ref: getSchemaPath(PageDto) }, { properties: { data: { type: 'array', items: { $ref: getSchemaPath(model) } } } }, ], }, }), ); ``` Use it as `@ApiPaginatedRe...Votes: 0GitHub stars: 549
- Nestjs DocumentationBuild separate Swagger documents from the same application by selecting controllers/modules for each audience and mounting them at different routes. For example, create a public document with public modules and an internal document with internal modules, then call `SwaggerModule.setup('docs/public', app, publicDoc)` and `SwaggerModule.setup('docs/internal', app, internalDoc)`. Give each document its own title/version and security scheme. Protect internal docs with network/auth controls and di...Votes: 0GitHub stars: 549
- Nestjs Error HandlingImplement a strictly typed global filter and use `HttpAdapterHost` so it works with Express or Fastify without importing platform-specific request/response types. ```ts @Catch() export class GlobalExceptionFilter implements ExceptionFilter { constructor(private readonly adapterHost: HttpAdapterHost) {} catch(exception: unknown, host: ArgumentsHost) { const { httpAdapter } = this.adapterHost; const ctx = host.switchToHttp(); const status = exception instanceof HttpException ? exception.getStat...Votes: 0GitHub stars: 549
- Nestjs Error HandlingKeep the layers separate: services throw domain-specific errors, an interceptor translates persistence/transport errors to Nest HTTP exceptions, and one global filter formats the final response. Use built-ins such as `NotFoundException`, `ForbiddenException`, and `BadRequestException` for HTTP-facing failures. Do not make services depend on `Request`/`Response` or throw transport-specific exceptions for ordinary domain rules. The final response should have one stable shape, for example `statu...Votes: 0GitHub stars: 549
- Nestjs Error HandlingNever include stack traces, internal file paths, SQL, or raw variable dumps in production HTTP responses. In the global filter, return a stable sanitized error contract and log the full exception server-side with the class context. ```ts const message = process.env.NODE_ENV === 'production' ? 'Internal server error' : exception instanceof Error ? exception.message : 'Request failed'; const stack = process.env.NODE_ENV === 'production' ? undefined : exception instanceof Error ? exception.stack...Votes: 0GitHub stars: 549
- Nestjs File UploadsDo not trust the filename or the client-provided `Content-Type`. Set a Multer size limit and inspect the file's magic bytes with `file-type`/`mmmagic` before accepting it. ```ts @Post() @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 5_000_000 } })) upload(@UploadedFile() file: Express.Multer.File) { const detected = await fileTypeFromBuffer(file.buffer); if (!detected || !['image/png', 'image/jpeg', 'application/pdf'].includes(detected.mime)) { throw new BadRequestException('U...Votes: 0GitHub stars: 549
- Nestjs File UploadsDo not buffer large uploads in Multer `MemoryStorage`. For files over roughly 10 MB, have the API issue a short-lived signed S3/GCS URL and let the client upload directly, or stream through `busboy`/`multer-s3` with strict size limits. The API should authenticate the upload request, constrain size and allowed types, generate object keys server-side, and avoid trusting MIME headers. If the API must proxy the stream, apply backpressure and never retain the complete file in the Node heap. Direct...Votes: 0GitHub stars: 549
- Nestjs File UploadsMake the request only persist the upload and enqueue a `FileUploadedEvent`; do not resize or transcode inside the HTTP handler. ```ts await files.saveMetadata(fileId, objectKey); try { await queue.add(FILE_UPLOADED, { fileId, objectKey }, { attempts: 3, backoff: { type: 'exponential', delay: 1000 } }); } catch (error) { await files.markProcessingEnqueueFailed(fileId, error); } return { fileId, status: 'queued' }; ``` A BullMQ worker downloads the object, processes it, validates the result, an...Votes: 0GitHub stars: 549
- Nestjs NotificationImplement notifications as a dual write: save the in-app notification first, then attempt FCM delivery without allowing an FCM outage to fail the request. ```ts const notification = await this.repo.save({ userId, type: NotificationType.APPOINTMENT_REMINDER, metadata: JSON.stringify({ appointmentId }), }); if (user.fcmToken) { try { await this.fcm.send({ token: user.fcmToken, data: { notificationId: notification.id, type: notification.type, appointmentId: String(appointmentId), }}); } catch (e...Votes: 0GitHub stars: 549
- Nestjs NotificationUse a granular enum rather than `type: string`, so clients can choose stable icons, colors, routing, and behavior without guessing arbitrary values. ```ts export enum NotificationType { APPOINTMENT_REMINDER = 'APPOINTMENT_REMINDER', MESSAGE_RECEIVED = 'MESSAGE_RECEIVED', PAYMENT_FAILED = 'PAYMENT_FAILED', } export class NotificationEntity { @Column({ type: 'enum', enum: NotificationType }) type!: NotificationType; } ``` Keep routing metadata as a typed domain object in application code, seria...Votes: 0GitHub stars: 549