npm package: @siteone/ssr-server
Description: SSR server for React applications
Author: Viktor Bezdek
Latest version: 1.0.0

# @siteone/ssr-server [![NPM version][npm-image]][npm-url] [![Dependency Status][daviddm-image]][daviddm-url]

> SSR server for React Apollo applications

## Contents

- [@siteone/ssr-server [![NPM version][npm-image]][npm-url] [![Dependency Status][daviddm-image]][daviddm-url]](#siteonessr-server-npm-versionnpm-url-dependency-statusdaviddm-imagedaviddm-url)
  - [Contents](#contents)
  - [Features (docs WIP)](#features-docs-wip)
  - [Installation](#installation)
  - [Service routes](#service-routes)
    - [General](#general)
    - [Ping `/__ping`](#ping-ping)
    - [Metrics `/__stats`](#metrics-stats)
    - [Cache](#cache)
      - [Current cache index `/__cache/index`](#current-cache-index-cacheindex)
      - [Clear whole cache `/__cache/clear`](#clear-whole-cache-cacheclear)
      - [Remove single record from cache `/__cache/index/:target`](#remove-single-record-from-cache-cacheindextarget)
      - [Get cache performance `/__cache/performance`](#get-cache-performance-cacheperformance)
  - [Configuration](#configuration)
    - [How to structure config](#how-to-structure-config)
    - [Server](#server)
      - [`config/server.js`](#configserverjs)
    - [Sentry](#sentry)
      - [`sentry.js`](#sentryjs)
    - [Cache](#cache-1)
    - [Service endpoints credentials](#service-endpoints-credentials)
    - [Proxies (docs WIP)](#proxies-docs-wip)
    - [Beforewares (docs WIP)](#beforewares-docs-wip)
    - [Custom routes (docs WIP)](#custom-routes-docs-wip)
    - [Static files serving (docs WIP)](#static-files-serving-docs-wip)
    - [Logging (docs WIP)](#logging-docs-wip)
    - [Redirects](#redirects)
      - [`route`](#route)
      - [`target`](#target)
      - [`status`](#status)
      - [`method`](#method)
      - [`qsa`](#qsa)
    - [SSR config (docs WIP)](#ssr-config-docs-wip)
  - [License](#license)

---

## Features (docs WIP)

- robust and flexible logging
- cache management endpoints
- caching backed by _Redis_ and provided via middleware based on `apicache` extended to support multi-instance apps

---

## Installation

```sh
$ npm install --save @siteone/ssr-server
```

---

## Service routes

### General

Each service route except _ping_ is secured via HTTP Basic auth. Credentials are provided in [configuration](#configuration) in section `serviceCredentials`.

**How to programmatically use/consume service endpoints**

```javascript
const request = require('request')

const credentials = 'admin:siteonejenejlepsi' // service credentials
const buff = new Buffer(data)
const authHash = buff.toString('base64') // authorization hash

const options = {
  method: 'GET',
  url: 'http://localhost:3000/__cache/index',
  headers: {
    Authorization: `Basic ${authHash}`,
  },
}
request(options, function(error, response) {
  if (error) throw new Error(error)
  console.log(response.body) // Data from service endpoint
})
```

### Ping `/__ping`

Responds with plain text string `pong`. Useful for monitoring response time or whether service is operational.

### Metrics `/__stats`

> SSR server exposes endpoint containing various useful metrics for monitoring systems/dashboards.

**Sample output:**

```json
{
  "server": {
    "status": "up",
    "name": "my-website-on-ssr-server",
    "version": "2.2.2",
    "startedAt": 1579700306454,
    "uptime": 693,
    "env": "development",
    "requests": {
      "total": 1,
      "lastMinute": 0,
      "lastFiveMins": 0,
      "lastFifteenMins": 0
    }
  },
  "node": {
    "version": "v12.14.1",
    "memoryUsage": "82M",
    "uptime": 693.779505301
  },
  "system": {
    "loadavg": [5.06103515625, 7.853515625, 11.1982421875],
    "freeMemory": "72M",
    "hostname": "Viktor-MacBook-Pro.local"
  }
}
```

### Cache

> SSR server provides endpoints to display cache index and invalidate both single route or whole cached data.

#### Current cache index `/__cache/index`

**Sample output:**

```json
{
  "all": [
    "/produkty/sporici-ucet/$$appendKey=desktop__walkin__internal__staging",
    "/platby-mobilem-a-hodinkami/$$appendKey=desktop__walkin__internal__staging"
  ],
  "groups": {}
}
```

#### Clear whole cache `/__cache/clear`

Clears whole cache and returns new cache index.

**Expected output if clearing was succesful:**

```json
{
  "all": [],
  "groups": {}
}
```

#### Remove single record from cache `/__cache/index/:target`

Please note, that `target` must be URL encoded cache key. Ex: `/__cache/clear/%2Fprodukty%2Fsporici-ucet%2F%24%24appendKey%3Ddesktop__walkin__internal__staging`. Endpoint response is new cache index.

**Example output:**

```json
{
  "all": [
    "/platby-mobilem-a-hodinkami/$$appendKey=desktop__walkin__internal__staging"
  ],
  "groups": {}
}
```

#### Get cache performance `/__cache/performance`

Returns cache performance metrics as JSON. Requires `trackPerformance` option to be enabled. WARNING: super cool feature, but may cause memory overhead issues. Don't use in production.

**Example output:**

```json
[
    {
        "lastCacheHit": "/test-slamic$$appendKey=unknown",
        "lastCacheMiss": null,
        "callCount": 1,
        "hitCount": 1,
        "missCount": 0,
        "hitRate": 1,
        "hitRateLast100": 1,
        "hitRateLast1000": 1,
        "hitRateLast10000": 1,
        "hitRateLast100000": 1
    }
]
```

---

## Configuration

> SSR server can be configured in many ways via configuration object.

### How to structure config

> Configuration object is large. Here's example how to compose it in readable way.

Create a `config` folder next to server entry point. Create new file for _each configured topic_. Combine them via object spreading in `config/index.js`.

### Server

- `port` _Number_
  - Port number to attach to
  - default `3000`
- `host`: _String_
  - Bind to host
  - `0.0.0.0` to be discoverable on network
  - `127.0.0.1` to local discovery only
  - default `0.0.0.0`

#### `config/server.js`

```javascript
export default {
  // Server config
  server: {
    port: 3005, // Port
    host: '0.0.0.0', // Host
  },
}
```

### Sentry

- `enabled`: _Boolean_
  - Is Sentry logging enabled
  - default `false`
- `dsn`: _String_
  - Sentry project DSN
  - default value `undefined`

#### `sentry.js`

```javascript
export default {
  sentry: {
    enabled: false,
    dsn: 'https://<key>@sentry.siteone.cz/<project>',
  },
}
```

### Cache

- `enabled`: *Boolean*
  - Is caching enabled
  - default `false`
- `redisClient`: *optional*
  - if provided cache is saved to redis instead of memory
  - **required when running multi instance applications**
  - initialized [redis](https://www.npmjs.com/package/redis) client
  - default `undefined`
- `defaultDuration`: *String|Number*
  - Should be either a number (in ms) or a string that can be parsed using [ms](https://www.npmjs.com/package/ms)
  - default `1 hour`
- `headerBlacklist`: *Array<Object>*
  - List of headers that should never be cached
  - default `[]`
- `statusCodes.exclude`: *Array<Number>*
  - List status codes to specifically exclude (e.g. [404, 403] cache all responses unless they had a 404 or 403 status)
  - default `[]`
- `statusCodes.include`: *Array<Number>*
  - List status codes to require (e.g. [200] caches ONLY responses with a success/200 code)
  - default `[]`
- `trackPerformance`: Boolean
  - enable/disable performance tracking.
  - **WARNING: may cause memory overhead issues. Don't use in production.**
  - default `false`
- `headers`: Object
  - Header overwrites
  - default `{}`

```javascript
// Server side rendering cache
export default {
  ssrCache: {
    debug: false, // If true, enables console output
    defaultDuration: '1 hour', // Should be either a number (in ms) or a string, defaults to 1 hour
    enabled: true, // If false, turns off caching globally (useful on dev)
    redisClient: client,
    headerBlacklist: [], // OPTIONAL: List of headers that should never be cached
    statusCodes: {
      exclude: [], // List status codes to specifically exclude (e.g. [404, 403] cache all responses unless they had a 404 or 403 status)
      include: [200], // List status codes to require (e.g. [200] caches ONLY responses with a success/200 code)
    },
    trackPerformance: false, // Enable/disable performance tracking... WARNING: super cool feature, but may cause memory overhead issues
    headers: {
      //  'cache-control': 'no-cache', // Example of header overwrite
    },
  },
}
```

### Service endpoints credentials

All service endpoints are secured via username and password and verified via HTTP Basic auth header. **Set credentials to something secure.**

- `user`: *String*
  - username
  - default `admin`
- `password`: *String*
  - username
  - default `siteonejenejlepsi`


```javascript
export default {
  serviceCredentials: {
    user: 'admin',
    password: 'siteonejenejlepsi',
  }
}
```

### Proxies (docs WIP)

```javascript
export default {
  proxies: [
    // {
    //   method: 'all', // Specify method
    //   route: '/', // Route path, regex or express route format
    //   proxy: {}, // Config object passed to http-proxy-middleware
    // },
  ],
}
```

### Beforewares (docs WIP)

Custom express middlewares registered before main SSR middleware.

```javascript
export default {
  beforewares: [],
}
```

### Custom routes (docs WIP)

```javascript
export default {
  routes: [
    // {
    //   method: 'all', // Specify method
    //   route: '/', // Route path, regex or express route format
    //   handler: (req, res, next) => next(), // Express route handler or array of those
    // },
  ],
}
```

### Static files serving (docs WIP)

```javascript
export default {
  static: {
    root: path.join(process.cwd(), 'public'), // Static files root
    options: {
      immutable: true, // Are files in static folder immutable
      maxAge: ms('10 days'), // Cache max age
    },
  },
}
```

### Logging (docs WIP)

```javascript
export default {
  logs: {
    transports: [new winston.transports.Console()], // Winston transports
    // Winston log formatter functions
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.json()
    ),
    meta: false, // OPTIONAL: control whether you want to log the meta data about the request (default to true)
    msg: 'HTTP {{req.method}} {{req.url}}', // OPTIONAL: customize the default logging message. E.g. '{{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}'
    expressFormat: true, // Use the default Express/morgan request formatting. Enabling this will override any msg if true. Will only output colors with colorize set to true
    colorize: true, // Color the text and status code, using the Express/morgan color palette (text: gray, status: default green, 3XX cyan, 4XX yellow, 5XX red).
    // ignoreRoute: (req, res) => false, // Function which allows ignoring some routes based on request response data
  },
}
```

### Redirects

SSR server accepts config value `redirects` which is `Array` of objects with shape `{route: String, target: String, method: String, qsa: Boolean, status: Number}`.

#### `route`
The parameter `route` is a *string* and is **required**. It can contain parameters like `:id`, `:year([0-9]{4})?` or `:action(view|edit)`. It's basically just the same as a route you would pass to `app.get()`.

#### `target`
The parameter `target` is a *string* and is **required**. It can contain parameters like `:id`, `:year?` or `:action(view)`, where a `?` marks an optional parameter and `(someString)` is a default value.

The parameters get replaced by their respective counterparts in the route.

```javascript
app.redirect("/a/:id([0-9]+)?", "/b/:id(1)");
app.redirect("/c/:action(view|delete)?", "/d/:action?");
```

```
/a           -> /b/1
/a/100       -> /b/100
/c           -> /d
/c/view      -> /d/view
```

#### `status`
The parameter `status` is an *integer* and is **optional**. It is a **HTTP (redirection) status code**. It defaults to 301.

#### `method`
The parameter `method` is a string and is **optional**. It is a *VERB* as in express' router. It defaults to `all`.

#### `qsa`
The parameter `qsa` is a boolean and is **optional**. It defaults to `true`. If set to true, the query string will be appended. By default, it will be discarded.

### SSR config (docs WIP)

```javascript
export default {
  ssr: {
    muiSupport: false, // Add styles collection for material ui
    csrOnly: false, // Use only client side rendering
    applicationConfig: {},
    excludeRoutes: [/pagebuilder/ig, /page-preview/ig], // Routes (array of regexes to exclude from SSR)
    useCsrOnError: true, // Use client side rendering when SSR fails (overriden by csr only)
    apolloClientFactory: () => true, // Factory function which creates configured apollo client
    ApplicationRoot: () => true, // React root component
    configureStore: () => true, // Configure Redux store function
    createInitialState: req => ({}), // OPTIONAL: Function which creates Redux initial state from request data
    bodyExtras: '', // extra markup to put in body
    headExtras: '', // extra markup to put in head
    nojsWarning: 'Zdá se, že máte vypnutý javascript. Zapněte si jej, aby vám tento web fungoval správně.',
    // HTML templates for all response type scenarios
    templates: {
      page: template, // General page where application can be rendered
      unsupportedBrowser: opts =>
        template({ ...opts, prerenderedMarkup: 'Unsupported browser' }), // Template when user is browsing with unsupported browser
      notFound: opts =>
        template({ ...opts, prerenderedMarkup: 'Page not found' }), // Template when page is not found
      error: opts => template({ ...opts, prerenderedMarkup: 'Fatal error' }), // General error template
    },
  }
}
```

---

## License

MIT © [SiteOne, s.r.o.](https://www.siteone.cz)

[npm-image]: https://badge.fury.io/js/%40siteone%2Fssr-server.svg
[npm-url]: https://npmjs.org/package/@siteone/ssr-server
[daviddm-image]: https://david-dm.org/siteone/ssr-server.svg?theme=shields.io
[daviddm-url]: https://david-dm.org/siteone/ssr-server