@nestjs-ssr/react 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2025 Georgi Alexandrov <https://github.com/georgialexandrov>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,587 @@
1
+ # @nestjs-ssr/react
2
+
3
+ **React SSR for NestJS that respects Clean Architecture.**
4
+
5
+ A lightweight, production-ready library that brings React to NestJS while preserving the architectural principles that make NestJS great: **Dependency Injection**, **SOLID principles**, and **clear separation of concerns**.
6
+
7
+ ## Why This Library?
8
+
9
+ ### Built for Clean Architecture
10
+
11
+ If you value [Uncle Bob's Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) and software craftsmanship, you'll appreciate how this library maintains architectural boundaries:
12
+
13
+ **Clear Separation of Concerns:**
14
+ ```typescript
15
+ // Server logic stays in controllers (Application Layer)
16
+ @Controller()
17
+ export class UserController {
18
+ constructor(private userService: UserService) {} // Proper DI
19
+
20
+ @Get('/users/:id')
21
+ @Render('views/user-profile')
22
+ async getUserProfile(@Param('id') id: string) {
23
+ const user = await this.userService.findById(id); // Business logic
24
+ return { user }; // Pure data - no rendering concerns
25
+ }
26
+ }
27
+
28
+ // View logic stays in React components (Presentation Layer)
29
+ export default function UserProfile({ data }: PageProps<{ user: User }>) {
30
+ return <div>{data.user.name}</div>; // Pure presentation
31
+ }
32
+ ```
33
+
34
+ **No Server/Client Confusion:**
35
+ - Server code is server code (Controllers, Services, Guards)
36
+ - Client code is client code (React components, hooks)
37
+ - No `'use server'` / `'use client'` directives scattered throughout
38
+ - No mixing of database queries with JSX
39
+ - No hidden boundaries where code "magically" switches execution context
40
+
41
+ **True Dependency Injection:**
42
+ ```typescript
43
+ // Use NestJS DI throughout your application
44
+ @Injectable()
45
+ export class ProductService {
46
+ constructor(
47
+ private db: DatabaseService,
48
+ private cache: CacheService,
49
+ ) {}
50
+ }
51
+
52
+ // Testable, mockable, follows IoC principle
53
+ ```
54
+
55
+ ### Why Not Next.js?
56
+
57
+ Next.js is excellent for many use cases, but if you care about architectural integrity:
58
+
59
+ **Next.js encourages coupling:**
60
+ ```tsx
61
+ // ❌ Server and client code mixed in the same file
62
+ export default function Page() {
63
+ const data = await fetch('...').then(r => r.json()); // Server
64
+ const [count, setCount] = useState(0); // Client
65
+ return <button onClick={() => setCount(count + 1)}>{data.title}</button>;
66
+ }
67
+ ```
68
+ - Server and client logic intertwined
69
+ - No dependency injection - just imports and function calls
70
+ - Difficult to test business logic in isolation
71
+ - Routes are files, not proper routing with guards/interceptors
72
+ - Framework-specific patterns instead of universal principles
73
+
74
+ **NestJS SSR maintains boundaries:**
75
+ ```tsx
76
+ // ✅ Controller: Server-only, testable, uses DI
77
+ @Controller()
78
+ export class ProductController {
79
+ constructor(private productService: ProductService) {}
80
+
81
+ @Get('/products')
82
+ @UseGuards(AuthGuard) // Proper middleware
83
+ @Render('views/products')
84
+ async list() {
85
+ return { products: await this.productService.findAll() };
86
+ }
87
+ }
88
+
89
+ // ✅ View: Client-only, pure presentation
90
+ export default function Products({ data }: PageProps) {
91
+ const [selected, setSelected] = useState(null);
92
+ return <ProductList products={data.products} onSelect={setSelected} />;
93
+ }
94
+ ```
95
+
96
+ ### Performance as a Bonus
97
+
98
+ Following clean architecture doesn't mean sacrificing performance. In our benchmarks:
99
+
100
+ - **NestJS SSR:** 3,050 req/sec (32ms latency)
101
+ - **Next.js:** 2,965 req/sec (33ms latency)
102
+ - **Remix:** 915 req/sec (109ms latency)
103
+
104
+ **Equal performance** with **better architecture.**
105
+
106
+ ## Features
107
+
108
+ ✅ **Architectural Integrity** - Respects SOLID and Clean Architecture principles
109
+ ✅ **Dependency Injection** - Full NestJS DI throughout your application
110
+ ✅ **Clear Boundaries** - Server code is server, client code is client
111
+ ✅ **Zero Configuration** - Works out of the box with sensible defaults
112
+ ✅ **TypeScript First** - Fully typed with excellent IDE support
113
+ ✅ **Streaming SSR** - Modern renderToPipeableStream support
114
+ ✅ **HMR in Development** - Powered by Vite for instant feedback
115
+ ✅ **Production Optimized** - Code splitting, compression, and caching
116
+ ✅ **Testable** - Easy to unit test controllers and services separately
117
+
118
+ ## Quick Start
119
+
120
+ ### Installation
121
+
122
+ ```bash
123
+ npm install @nestjs-ssr/react react react-dom vite
124
+ ```
125
+
126
+ ### 1. Configure Vite
127
+
128
+ ```typescript
129
+ // vite.config.ts
130
+ import { defineConfig } from 'vite';
131
+ import react from '@vitejs/plugin-react';
132
+ import { viewRegistryPlugin } from '@nestjs-ssr/react/vite';
133
+
134
+ export default defineConfig({
135
+ plugins: [react(), viewRegistryPlugin()],
136
+ });
137
+ ```
138
+
139
+ ### 2. Setup NestJS Module
140
+
141
+ ```typescript
142
+ // app.module.ts
143
+ import { Module } from '@nestjs/common';
144
+ import { RenderModule } from '@nestjs-ssr/react';
145
+
146
+ @Module({
147
+ imports: [
148
+ RenderModule.register(), // Zero config!
149
+ ],
150
+ })
151
+ export class AppModule {}
152
+ ```
153
+
154
+ ### 3. Create a View
155
+
156
+ ```typescript
157
+ // src/views/home.tsx
158
+ import type { PageProps } from '@nestjs-ssr/react';
159
+
160
+ interface HomeData {
161
+ message: string;
162
+ }
163
+
164
+ export default function Home({ data }: PageProps<HomeData>) {
165
+ return <h1>{data.message}</h1>;
166
+ }
167
+ ```
168
+
169
+ ### 4. Create a Controller
170
+
171
+ ```typescript
172
+ // app.controller.ts
173
+ import { Controller, Get } from '@nestjs/common';
174
+ import { Render } from '@nestjs-ssr/react';
175
+
176
+ @Controller()
177
+ export class AppController {
178
+ @Get()
179
+ @Render('views/home')
180
+ getHome() {
181
+ return { message: 'Hello from NestJS SSR!' };
182
+ }
183
+ }
184
+ ```
185
+
186
+ ### 5. Add Entry Files
187
+
188
+ Create these files in `src/view/`:
189
+
190
+ **entry-client.tsx**:
191
+ ```typescript
192
+ import { StrictMode } from 'react';
193
+ import { hydrateRoot } from 'react-dom/client';
194
+ import { viewRegistry } from './view-registry.generated';
195
+
196
+ const viewPath = window.__COMPONENT_PATH__;
197
+ const initialProps = window.__INITIAL_STATE__ || {};
198
+ const renderContext = window.__CONTEXT__ || {};
199
+
200
+ const ViewComponent = viewRegistry[viewPath];
201
+
202
+ if (!ViewComponent) {
203
+ throw new Error(`View "${viewPath}" not found in registry`);
204
+ }
205
+
206
+ hydrateRoot(
207
+ document.getElementById('root')!,
208
+ <StrictMode>
209
+ <ViewComponent data={initialProps} context={renderContext} />
210
+ </StrictMode>
211
+ );
212
+ ```
213
+
214
+ **entry-server.tsx**:
215
+ ```typescript
216
+ import { viewRegistry } from './view-registry.generated';
217
+
218
+ export function render(viewPath: string, props: any, context: any) {
219
+ const ViewComponent = viewRegistry[viewPath];
220
+
221
+ if (!ViewComponent) {
222
+ throw new Error(`View "${viewPath}" not found in registry`);
223
+ }
224
+
225
+ return <ViewComponent data={props} context={context} />;
226
+ }
227
+ ```
228
+
229
+ ### 6. Run
230
+
231
+ ```bash
232
+ npm run dev
233
+ ```
234
+
235
+ Visit [http://localhost:3000](http://localhost:3000) 🎉
236
+
237
+ ## Core Concepts
238
+
239
+ ### The `@Render` Decorator
240
+
241
+ The decorator intercepts your controller's response and renders it with React:
242
+
243
+ ```typescript
244
+ @Get('/users/:id')
245
+ @Render('users/views/user-profile')
246
+ async getUser(@Param('id') id: string) {
247
+ const user = await this.userService.findOne(id);
248
+ return { user }; // Passed as `data` prop to component
249
+ }
250
+ ```
251
+
252
+ ### Type-Safe Props
253
+
254
+ Components receive props with full TypeScript support:
255
+
256
+ ```typescript
257
+ import type { PageProps, RenderContext } from '@nestjs-ssr/react';
258
+
259
+ interface UserData {
260
+ user: User;
261
+ }
262
+
263
+ export default function UserProfile({ data, context }: PageProps<UserData>) {
264
+ return (
265
+ <div>
266
+ <h1>{data.user.name}</h1>
267
+ <p>Requested from: {context.path}</p>
268
+ </div>
269
+ );
270
+ }
271
+ ```
272
+
273
+ ### Request Context
274
+
275
+ Every component receives the request context:
276
+
277
+ ```typescript
278
+ interface RenderContext {
279
+ url: string;
280
+ path: string;
281
+ query: Record<string, any>;
282
+ params: Record<string, string>;
283
+ userAgent?: string;
284
+ acceptLanguage?: string;
285
+ referer?: string;
286
+ }
287
+ ```
288
+
289
+ You can extend it with custom data using TypeScript declaration merging!
290
+
291
+ ### React Hooks
292
+
293
+ Access context in any component:
294
+
295
+ ```typescript
296
+ import { usePageContext, useParams, useQuery } from '@nestjs-ssr/react';
297
+
298
+ function MyComponent() {
299
+ const context = usePageContext();
300
+ const params = useParams();
301
+ const query = useQuery();
302
+
303
+ return <div>User ID: {params.id}</div>;
304
+ }
305
+ ```
306
+
307
+ ## Configuration
308
+
309
+ ### SSR Modes
310
+
311
+ Choose between string mode (simple) or stream mode (faster):
312
+
313
+ ```typescript
314
+ RenderModule.register({
315
+ mode: 'stream', // or 'string' (default)
316
+ })
317
+ ```
318
+
319
+ ### Custom Error Pages
320
+
321
+ ```typescript
322
+ import { ErrorPageCustom } from './error-page';
323
+
324
+ RenderModule.register({
325
+ errorPageDevelopment: ErrorPageCustom,
326
+ errorPageProduction: ErrorPageCustom,
327
+ })
328
+ ```
329
+
330
+ ### Environment Variables
331
+
332
+ ```bash
333
+ SSR_MODE=stream # 'string' or 'stream'
334
+ NODE_ENV=production # Enables production optimizations
335
+ ```
336
+
337
+ ### Vite Development Setup
338
+
339
+ You have two options for integrating Vite during development:
340
+
341
+ #### Option 1: Simple Setup (Recommended for Getting Started)
342
+
343
+ Use Vite in middleware mode directly within NestJS. This approach is simpler but requires manual page refresh to see changes.
344
+
345
+ ```typescript
346
+ // main.ts
347
+ import { createServer as createViteServer } from 'vite';
348
+ import { RenderService } from '@nestjs-ssr/react';
349
+
350
+ async function bootstrap() {
351
+ const app = await NestFactory.create(AppModule);
352
+ const renderService = app.get(RenderService);
353
+
354
+ if (process.env.NODE_ENV !== 'production') {
355
+ const vite = await createViteServer({
356
+ server: { middlewareMode: true },
357
+ appType: 'custom',
358
+ });
359
+
360
+ app.use(vite.middlewares);
361
+ renderService.setViteServer(vite);
362
+ }
363
+
364
+ await app.listen(3000);
365
+ }
366
+ ```
367
+
368
+ **Pros:**
369
+ - Simple setup (~10 lines of code)
370
+ - Single server on port 3000
371
+ - Auto-restart on file changes
372
+
373
+ **Cons:**
374
+ - Requires manual page refresh to see changes
375
+ - No hot module replacement (HMR)
376
+
377
+ See [`minimal-simple`](../../examples/minimal-simple) example.
378
+
379
+ #### Option 2: Full HMR Setup (Best Developer Experience)
380
+
381
+ Run a separate Vite dev server with proxy middleware for instant hot reloading.
382
+
383
+ ```typescript
384
+ // main.ts
385
+ import { createServer as createViteServer } from 'vite';
386
+ import { RenderService } from '@nestjs-ssr/react';
387
+
388
+ async function bootstrap() {
389
+ const app = await NestFactory.create(AppModule);
390
+ const renderService = app.get(RenderService);
391
+
392
+ if (process.env.NODE_ENV !== 'production') {
393
+ // Proxy to external Vite server for HMR
394
+ const { createProxyMiddleware } = await import('http-proxy-middleware');
395
+ const viteProxy = createProxyMiddleware({
396
+ target: 'http://localhost:5173',
397
+ changeOrigin: true,
398
+ ws: true,
399
+ pathFilter: (pathname: string) => {
400
+ return (
401
+ pathname.startsWith('/src/') ||
402
+ pathname.startsWith('/@') ||
403
+ pathname.startsWith('/node_modules/')
404
+ );
405
+ },
406
+ });
407
+ app.use(viteProxy);
408
+
409
+ // Vite instance for SSR
410
+ const vite = await createViteServer({
411
+ server: { middlewareMode: true },
412
+ appType: 'custom',
413
+ });
414
+ renderService.setViteServer(vite);
415
+ }
416
+
417
+ await app.listen(3000);
418
+ }
419
+ ```
420
+
421
+ **Additional dependencies:**
422
+ ```bash
423
+ npm install -D http-proxy-middleware concurrently
424
+ ```
425
+
426
+ **package.json scripts:**
427
+ ```json
428
+ {
429
+ "scripts": {
430
+ "start:dev": "concurrently \"vite --port 5173\" \"nest start --watch\""
431
+ }
432
+ }
433
+ ```
434
+
435
+ **vite.config.js:**
436
+ ```javascript
437
+ export default defineConfig({
438
+ server: {
439
+ port: 5173,
440
+ strictPort: true,
441
+ hmr: { port: 5173 }, // Critical for HMR
442
+ },
443
+ });
444
+ ```
445
+
446
+ **Pros:**
447
+ - Instant hot module replacement (HMR)
448
+ - Best developer experience for rapid iteration
449
+ - No page refresh needed
450
+
451
+ **Cons:**
452
+ - More complex setup (~40 lines)
453
+ - Requires two servers (NestJS + Vite)
454
+ - Additional dependencies
455
+
456
+ See [`minimal`](../../examples/minimal) example.
457
+
458
+ ## Advanced Features
459
+
460
+ ### Error Monitoring (Coming in v0.2.0)
461
+
462
+ Error monitoring integration is planned for the next release. It will support integrations with Sentry, Datadog, and custom error reporters.
463
+
464
+ For now, you can use NestJS's built-in exception filters and logging for error handling.
465
+
466
+ ### Extending Context
467
+
468
+ Add custom data to the render context:
469
+
470
+ ```typescript
471
+ // types/render-context.d.ts
472
+ declare module '@nestjs-ssr/react' {
473
+ interface RenderContext {
474
+ user?: User;
475
+ tenant?: string;
476
+ }
477
+ }
478
+ ```
479
+
480
+ Then inject it in a custom interceptor.
481
+
482
+ ## Examples
483
+
484
+ Choose the example that matches your needs:
485
+
486
+ - **[Minimal Simple](../../examples/minimal-simple/)** - Simplest setup with Vite middleware (no HMR)
487
+ - Perfect for getting started quickly
488
+ - Single server, minimal configuration
489
+
490
+ - **[Minimal](../../examples/minimal/)** - Full HMR setup with dual-server architecture
491
+ - Best developer experience with instant hot reloading
492
+ - Recommended for active development
493
+
494
+ - **[Full-Featured](../../examples/full-featured/)** - Production-ready example
495
+ - Security headers, caching, error handling
496
+ - Streaming SSR with React Suspense
497
+
498
+ ## Documentation
499
+
500
+ - [Getting Started](../../docs/getting-started.md)
501
+ - [Why NestJS SSR?](../../docs/why-nestjs-ssr.md)
502
+ - [Architecture](../../docs/ARCHITECTURE.md)
503
+ - [Testing Guide](../../docs/TESTING_STRATEGY.md)
504
+ - [Production Deployment](../../docs/PRODUCTION_RISKS.md)
505
+
506
+ ## Philosophy
507
+
508
+ This package is built on two foundational principles:
509
+
510
+ ### Clean Architecture & SOLID Principles
511
+
512
+ Following [Uncle Bob's Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) and software craftsmanship principles:
513
+
514
+ **Single Responsibility Principle (SRP):**
515
+ - Controllers handle HTTP routing and orchestration
516
+ - Services contain business logic
517
+ - Views handle presentation
518
+ - Each has one reason to change
519
+
520
+ **Dependency Inversion Principle (DIP):**
521
+ - Controllers depend on service abstractions (interfaces)
522
+ - Services are injected via NestJS DI
523
+ - Views receive data as props (dependency injection via props)
524
+ - No concrete dependencies on frameworks in your business logic
525
+
526
+ **Interface Segregation & Open/Closed:**
527
+ - Use NestJS Guards for authentication/authorization
528
+ - Use Interceptors for cross-cutting concerns
529
+ - Views are pure functions - open for extension, closed for modification
530
+
531
+ **Clear Architectural Boundaries:**
532
+ ```
533
+ ┌─────────────────────────────────────────┐
534
+ │ Presentation Layer (React Components) │
535
+ │ - Pure presentation logic │
536
+ │ - Client-side interactivity │
537
+ │ - No business logic │
538
+ └─────────────────────────────────────────┘
539
+ ↓ Props (Data Flow)
540
+ ┌─────────────────────────────────────────┐
541
+ │ Application Layer (Controllers) │
542
+ │ - Request handling │
543
+ │ - Orchestration │
544
+ │ - DTO validation │
545
+ └─────────────────────────────────────────┘
546
+ ↓ DI (Dependency Injection)
547
+ ┌─────────────────────────────────────────┐
548
+ │ Domain Layer (Services) │
549
+ │ - Business logic │
550
+ │ - Domain models │
551
+ │ - Use cases │
552
+ └─────────────────────────────────────────┘
553
+ ```
554
+
555
+ ### UnJS Philosophy
556
+
557
+ Following the [UnJS philosophy](https://unjs.io/) for modern JavaScript libraries:
558
+
559
+ 1. **Unintrusive** - Integrates seamlessly with existing NestJS apps
560
+ 2. **Zero-Config** - Works out of the box with sensible defaults
561
+ 3. **Fully Extensible** - Customize everything when needed
562
+ 4. **Framework Agnostic** - No opinions on routing, state, or business logic
563
+ 5. **TypeScript First** - Excellent type safety and IDE support
564
+
565
+ **The Result:** Clean, maintainable, testable code that scales with your team and product.
566
+
567
+ ## Requirements
568
+
569
+ - Node.js 18+
570
+ - NestJS 11+
571
+ - React 19+
572
+ - Vite 6+
573
+ - TypeScript 5+
574
+
575
+ ## License
576
+
577
+ MIT © Georgi Alexandrov
578
+
579
+ ## Contributing
580
+
581
+ Contributions welcome! Please see [CONTRIBUTING.md](../../CONTRIBUTING.md).
582
+
583
+ ## Support
584
+
585
+ - [GitHub Issues](https://github.com/georgialexandrov/nestjs-ssr/issues)
586
+ - [Documentation](../../docs/)
587
+ - [Examples](../../examples/)