npm package: @quvel-kit/ssr
Description: SSR utilities and services for Quvel UI
Author: Irving Gomez
Latest version: 1.1.6

# @quvel-kit/ssr

Server-side rendering infrastructure for Quasar applications. Provides a service container, plugin architecture, and request lifecycle management for building SSR applications.

## Philosophy

SSR infrastructure should be **extensible without coupling**. Core services handle logging, API communication, and request orchestration. Features like multi-tenancy, analytics, or authentication extend through plugins—no tight coupling, no bloat.

## Core Concepts

### Service Container

The container manages service lifecycle and dependency injection. Services register once at startup, boot asynchronously with configuration, and remain available for the application lifetime.

**Two phases prevent initialization race conditions:**

- **Register:** Services get references to dependencies. Order matters—if ServiceA needs ServiceB, register ServiceB first.
- **Boot:** Services perform async initialization. All dependencies are now available.

This pattern ensures services can safely depend on each other without ordering bugs.

### Plugin System

Plugins package related functionality into self-contained units. Each plugin provides a name and services. SSR core has zero knowledge of what plugins do—complete separation of concerns.

A plugin is just an object:

```typescript
{
  name: 'my-feature',
  services: [
    { name: 'MyService', instance: new MyService(config) }
  ]
}
```

Plugins register themselves through configuration. SSR handles the registration automatically.

### Hook-Based Request Pipeline

Request handling follows a hook pattern. Services register callbacks for specific lifecycle events. Multiple services can hook into the same event—each runs in registration order.

**Available hooks (execution order):**

1. **`onPreRender(req, windowBag, res)`** - Fires before Vue/React renders. Use for injecting data into `window` object via WindowBag, setting response headers, or preparing request context (tenant resolution, trace generation, etc.). The WindowBag provides a `mergeTo()` method for merging data into existing window objects like `__APP_CONFIG__`.

2. **`onPostRender(html, req, res)`** - Fires after rendering and WindowBag injection. Receives an `HtmlTransformer` instance with ergonomic helpers for HTML modification:

```typescript
handler.onPostRender((html, req, res) => {
  // Add scripts at various positions
  html.addScript({
    content: 'console.log("App loaded")',
    position: 'beforeBodyEnd',
    async: true
  });

  // Add meta tags
  html.addMeta({
    name: 'description',
    content: 'My application'
  });

  // Add link tags (stylesheets, etc)
  html.addLink({
    rel: 'stylesheet',
    href: '/custom.css'
  });

  // Add inline styles
  html.addStyle({
    content: 'body { margin: 0; }',
    position: 'beforeHeadEnd'
  });

  // Advanced: modify raw HTML
  html.modifyRaw((rawHtml) => {
    return rawHtml.replace(/old/g, 'new');
  });
});
```

**Script positions:**
- `'afterHeadStart'` - Right after `<head>` tag
- `'beforeHeadEnd'` - Just before `</head>` tag
- `'afterBodyStart'` - Right after `<body>` tag
- `'beforeBodyEnd'` - Just before `</body>` tag

Hooks receive the Express request and response objects. Mutations to request context are visible to all subsequent hooks and the renderer.

## Installation

```bash
yarn add @quvel-kit/ssr
```

## Configuration

Create `src-ssr/ssr.config.ts`:

```typescript
import { defineSSRConfig } from '@quvel-kit/ssr';
import { createTenancyPlugin } from '@quvel-kit/tenancy';

export default defineSSRConfig({
  plugins: [
    createTenancyPlugin(), // Uses TENANCY_* env vars by default
  ],

  services: [
    // Only add custom services here
    // Core services auto-register
  ],

  express: {
    enableCompression: true,
    trustProxy: true,
  },
});
```

### Environment Variables

All configuration values have environment variable defaults. Override declaratively when needed.

**Logging:**
- `SSR_LOG_TYPE` - Logger type (default: `console`)
- `SSR_LOG_LEVEL` - Log level (default: `info`)

**API:**
- `VITE_API_URL` - Public API URL
- `VITE_INTERNAL_API_URL` - Internal API URL for SSR→backend
- `SSR_API_KEY` - API key for internal requests

**Express:**
Configuration has smart defaults. Override only when needed:
```typescript
express: {
  enableCompression: true,  // Default: true in prod, false in dev
  enableCors: false,         // Default: false
  trustProxy: false,         // Enable when behind nginx/load balancer
  strictGetOnly: false,      // Restrict to GET requests only
  enableHelmet: true,        // Security headers (default: true)
}
```

## Server Integration

In `src-ssr/server.ts`:

```typescript
import { initSSRContainer, destroySSRContainer } from '@quvel-kit/ssr';
import ssrConfig from './ssr.config.js';

export const create = defineSsrCreate(async () => {
  const serviceMap = new Map(
    (ssrConfig.services || []).map(service => {
      const name = typeof service === 'function' ? service.name : service.constructor.name;
      return [name, service];
    })
  );

  const app = express();

  await initSSRContainer(app, ssrConfig, serviceMap);

  return app;
});

export const close = defineSsrClose(({ listenResult }) => {
  destroySSRContainer();
  return listenResult.close();
});
```

Create `src-ssr/middlewares/render.ts`:

```typescript
import { defineSsrMiddleware } from '#q-app/wrappers';
import { getSSRContainer } from '@quvel-kit/ssr';
import { SSRRequestHandler } from '@quvel-kit/ssr';

export default defineSsrMiddleware(({ app, resolve, render, serve }) => {
  app.get(resolve.urlPath('*'), (req, res) => {
    void (async () => {
      try {
        const container = getSSRContainer();
        const handler = container.get(SSRRequestHandler);

        await handler.handleRequest(req, res, render);
      } catch (err) {
        const error = err as any;

        if (error.url) {
          res.redirect(error.code || 302, error.url);
        } else if (error.code === 404) {
          res.status(404).send('404 | Page Not Found');
        } else if (process.env.DEV) {
          serve.error({ err: error, req, res });
        } else {
          res.status(500).send('500 | Internal Server Error');
        }
      }
    })();
  });
});
```

## Creating Plugins

Plugins are functions that return a plugin object. They typically read from environment variables with user overrides.

```typescript
import type { SSRPlugin } from '@quvel-kit/ssr';

export function createMyPlugin(userConfig = {}): SSRPlugin {
  // Read env vars
  const apiKey = process.env.MY_PLUGIN_API_KEY;
  const enabled = process.env.MY_PLUGIN_ENABLED !== 'false';

  // Merge with user overrides
  const config = {
    apiKey,
    enabled,
    ...userConfig
  };

  return {
    name: 'my-feature',
    services: [
      { name: 'MyService', instance: new MyService(config) }
    ]
  };
}
```

Users enable your plugin with minimal config:

```typescript
import { createMyPlugin } from '@my-org/my-plugin';

export default defineSSRConfig({
  plugins: [
    createMyPlugin(), // Uses MY_PLUGIN_* env vars
  ],
});
```

## Creating Services

Services extend `SSRService` and implement lifecycle methods:

```typescript
import { SSRService } from '@quvel-kit/ssr';
import type { ISSRContainer, SSRSingletonService } from '@quvel-kit/ssr';

export class MyService extends SSRService implements SSRSingletonService {
  private logger!: ILogger;
  private apiService!: SSRApiService;

  override register(container: ISSRContainer): void {
    // Get dependencies (just references)
    this.logger = container.get(SSRLogService).createLogger('MyService');
    this.apiService = container.get(SSRApiService);

    // Register hooks
    const handler = container.get(SSRRequestHandler);
    handler.onPreRender(this.handlePreRender.bind(this));
  }

  override async boot(): Promise<void> {
    // Async initialization
    this.logger.info('MyService booted');
  }

  private async handlePreRender(req: SSRRequest, windowBag: WindowBag, res: Response): Promise<void> {
    // Handle pre-render lifecycle event
    windowBag.set('MY_DATA', { initialized: true });
  }
}
```

### Service Lifecycle

The container manages three phases automatically when you call `initSSRContainer()`:

**1. Initialize**
- Services are instantiated from their classes
- Constructors run, basic object setup happens
- No dependencies available yet

**2. Register**
- `register(container)` is called on each service in order
- Services get references to dependencies from the container
- Services register hooks with SSRRequestHandler
- Services set up local state
- **Don't call async methods on dependencies—they're not booted yet**

**3. Boot**
- `boot()` is called on each service (if implemented)
- All services are now registered and ready
- Safe to call async methods on dependencies
- Perform async initialization (preload data, connect to external services, etc.)

This three-phase pattern prevents initialization race conditions. If ServiceA needs ServiceB, just register ServiceB first in your config—the container ensures everything boots in the correct order.

### Hook Pattern

Services hook into the request lifecycle through `SSRRequestHandler`:

```typescript
override register(container: ISSRContainer): void {
  const handler = container.get(SSRRequestHandler);

  handler.onPreRender(async (req, windowBag, res) => {
    // Runs before rendering - inject data to client
    windowBag.set('MY_DATA', { foo: 'bar' });

    // Merge into existing window objects (e.g., AppConfig)
    windowBag.mergeTo('__APP_CONFIG__', {
      customField: 'value'
    });

    // Or set response headers
    res.setHeader('X-Custom-Header', 'value');
  });

  handler.onPostRender((html, req, res) => {
    // Runs after rendering - transform HTML
    html.addScript({
      src: '/analytics.js',
      position: 'beforeBodyEnd',
      async: true
    });
  });
}
```

The WindowBag passes data to client-side JavaScript. Set values that will be available as `window.__MY_DATA__` on the client.

## Core Services

Auto-registered services available in all applications:

**SSRLogService** - Structured logging with trace IDs
```typescript
const logger = container.get(SSRLogService).createLogger('MyApp');
logger.info('Request handled', { userId: 123 });
```

**SSRApiService** - Axios-based HTTP client
```typescript
const api = container.get(SSRApiService);
const data = await api.getAxiosInstance().get('/users');
```

**SSRRequestHandler** - Request lifecycle orchestration
```typescript
const handler = container.get(SSRRequestHandler);
handler.onPreRender(async (req, windowBag, res) => { /* ... */ });
handler.onPostRender(async (html, req, res) => { /* ... */ });
```

**SSRTraceService** - Distributed tracing for SSR requests
```typescript
// Automatically generates trace IDs, sets X-Trace-ID header, and injects into AppConfig
// Trace info is injected into window.__APP_CONFIG__.trace for client access
// Access trace info from request context:
req.requestContext?.appConfig?.trace // { id, timestamp, environment, runtime, tenant }
```

**AppConfigResolver** - Resolves app configuration
```typescript
const resolver = container.get(AppConfigResolver);
const config = await resolver.resolveConfig(req);
```

## Development

```bash
# Build
yarn build

# Watch mode
yarn dev

# Clean
yarn clean
```

## License

MIT