Skip to content

Commit

Permalink
Example: Passing data from server through API (#2594)
Browse files Browse the repository at this point in the history
* Add example on how to pass data through js api during SSR

Requested in #1117

* Use content negotiation instead of a separate route

* Codereview feedback

* Move security related test cases into a its own file.

* Removes the unused renderScript function

* Add a nerv example. (#3573)

* Add a nerv example.

* Fix for indentation/style

* Fix for name
  • Loading branch information
unregistered authored and timneutkens committed Feb 3, 2018
1 parent d103345 commit 7afc008
Show file tree
Hide file tree
Showing 15 changed files with 333 additions and 35 deletions.
50 changes: 50 additions & 0 deletions examples/pass-server-data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/pass-server-data)

# Pass Server Data Directly to a Next.js Page during SSR

## How to use

Download the example [or clone the repo](https://github.com/zeit/next.js):

```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/pass-server-data
cd pass-server-data
```

Install it and run:

```bash
npm install
npm run dev
```

Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))

```bash
now
```

## The idea behind the example

If you already have a custom server which has local data (for instance cached data from an API call, or data read
from a file at startup) that you wish to make available in the Next.js page, you can pass that data in the query
parameter of `nextApp.render()`.

This is not the only way to pass data. You could also expose an endpoint and make a `fetch()` call to localhost, or you could
import server-side code with `eval` (necessary to prevent webpack from trying to package your server code). However both
solutions leave something to be desired in either performance or elegance.

This example shows the express server at `server.js` reading in a file at load time with static data (this could also have been
data cached from an API call) in `operations/get-item.js`. It has two routes: a home page, and an item page. The item page uses
data from the get-item operation, passed as a query parameter in `routes/item.js`.

We use this data in `pages/item.js` if rendered server-side, or make a fetch request if rendered client-side.
The server knows whether or not to use next.js to render the route based on the Accept header, which will be
`application/json` when we fetch client-side.

Take a look at the following files:

* server.js
* routes/item.js
* pages/item.js
* operations/get-item.js
5 changes: 5 additions & 0 deletions examples/pass-server-data/data/item.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Now",
"subtitle": "Realtime global deployments",
"seller": "Zeit"
}
12 changes: 12 additions & 0 deletions examples/pass-server-data/operations/get-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const fs = require('fs')

// In this case, data read from the fs, but it could also be a cached API result.
const data = fs.readFileSync('./data/item.json', 'utf8')
const parsedData = JSON.parse(data)

function getItem () {
console.log('Requested Item Data:', data)
return parsedData
}

module.exports = { getItem }
16 changes: 16 additions & 0 deletions examples/pass-server-data/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "pass-server-data",
"version": "1.0.0",
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
},
"dependencies": {
"express": "^4.14.0",
"isomorphic-fetch": "^2.2.1",
"next": "latest",
"react": "^15.4.2",
"react-dom": "^15.4.2"
}
}
8 changes: 8 additions & 0 deletions examples/pass-server-data/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react'
import Link from 'next/link'

export default () => (
<ul>
<li><Link href='/item'><a>View Item</a></Link></li>
</ul>
)
33 changes: 33 additions & 0 deletions examples/pass-server-data/pages/item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {Component} from 'react'
import Link from 'next/link'
import fetch from 'isomorphic-fetch'

export default class extends Component {
static async getInitialProps ({ req, query }) {
const isServer = !!req

console.log('getInitialProps called:', isServer ? 'server' : 'client')

if (isServer) {
// When being rendered server-side, we have access to our data in query that we put there in routes/item.js,
// saving us an http call. Note that if we were to try to require('../operations/get-item') here,
// it would result in a webpack error.
return { item: query.itemData }
} else {
// On the client, we should fetch the data remotely
const res = await fetch('/_data/item', {headers: {'Accept': 'application/json'}})
const json = await res.json()
return { item: json }
}
}

render () {
return (
<div className='item'>
<div><Link href='/'><a>Back Home</a></Link></div>
<h1>{this.props.item.title}</h1>
<h2>{this.props.item.subtitle} - {this.props.item.seller}</h2>
</div>
)
}
}
39 changes: 39 additions & 0 deletions examples/pass-server-data/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const express = require('express')
const next = require('next')
const api = require('./operations/get-item')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
const server = express()

// Set up home page as a simple render of the page.
server.get('/', (req, res) => {
console.log('Render home page')
return app.render(req, res, '/', req.query)
})

// Serve the item webpage with next.js as the renderer
server.get('/item', (req, res) => {
const itemData = api.getItem()
app.render(req, res, '/item', { itemData })
})

// When rendering client-side, we will request the same data from this route
server.get('/_data/item', (req, res) => {
const itemData = api.getItem()
res.json(itemData)
})

// Fall-back on other next.js assets.
server.get('*', (req, res) => {
return handle(req, res)
})

server.listen(3000, (err) => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})
44 changes: 44 additions & 0 deletions examples/using-nerv/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/using-nerv)

# Hello World example

## How to use

### Using `create-next-app`

Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:

```
npm i -g create-next-app
create-next-app --example using-nerv using-nerv-app
```

### Download manually

Download the example [or clone the repo](https://github.com/zeit/next.js):

```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/using-nerv
cd using-nerv
```

Install it and run:

```bash
npm install
npm run dev
```

Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))

```bash
now
```

## The idea behind the example

This example uses [Nerv](https://nerv.aotu.io/) instead of React. It's a "blazing fast React alternative, compatible with IE8 and React 16". Here we've customized Next.js to use Nerv instead of React.

Here's how we did it:

* Use `next.config.js` to customize our webpack config to support [Nerv](https://nerv.aotu.io/)
16 changes: 16 additions & 0 deletions examples/using-nerv/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = {
webpack: function (config, { dev }) {
// For the development version, we'll use React.
// Because, it supports react hot loading and so on.
if (dev) {
return config
}

config.resolve.alias = {
react: 'nervjs',
'react-dom': 'nervjs'
}

return config
}
}
21 changes: 21 additions & 0 deletions examples/using-nerv/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "using-nerv",
"version": "1.0.0",
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
},
"dependencies": {
"module-alias": "^2.0.0",
"next": "latest",
"nervjs": "^1.2.4",
"react": "^16.0.0",
"react-dom": "^16.0.0"
},
"license": "ISC",
"devDependencies": {
"react": "~15.6.1",
"react-dom": "~15.6.1"
}
}
5 changes: 5 additions & 0 deletions examples/using-nerv/pages/about.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react'

export default () => (
<div>About us</div>
)
6 changes: 6 additions & 0 deletions examples/using-nerv/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react'
import Link from 'next/link'

export default () => (
<div>Hello World. <Link href='/about'><a>About</a></Link></div>
)
29 changes: 29 additions & 0 deletions examples/using-nerv/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const moduleAlias = require('module-alias')

// For the development version, we'll use React.
// Because, it support react hot loading and so on.
if (!dev) {
moduleAlias.addAlias('react', 'nervjs')
moduleAlias.addAlias('react-dom', 'nervjs')
}

const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')

const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare()
.then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true)
handle(req, res, parsedUrl)
})
.listen(port, (err) => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
})
})
39 changes: 4 additions & 35 deletions test/integration/production/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ import {
nextBuild,
startApp,
stopApp,
renderViaHTTP,
waitFor
renderViaHTTP
} from 'next-test-utils'
import webdriver from 'next-webdriver'
import fetch from 'node-fetch'
import dynamicImportTests from '../../basic/test/dynamic'
import { readFileSync } from 'fs'
import security from './security'

const appDir = join(__dirname, '../')
let appPort
Expand Down Expand Up @@ -74,23 +73,6 @@ describe('Production Usage', () => {
})
})

describe('With XSS Attacks', () => {
it('should prevent URI based attaks', async () => {
const browser = await webdriver(appPort, '/\',document.body.innerHTML="HACKED",\'')
// Wait 5 secs to make sure we load all the client side JS code
await waitFor(5000)

const bodyText = await browser
.elementByCss('body').text()

if (/HACKED/.test(bodyText)) {
throw new Error('Vulnerable to XSS attacks')
}

browser.close()
})
})

describe('Misc', () => {
it('should handle already finished responses', async () => {
const res = {
Expand All @@ -111,21 +93,6 @@ describe('Production Usage', () => {
const data = await renderViaHTTP(appPort, '/static/data/item.txt')
expect(data).toBe('item')
})

it('should only access files inside .next directory', async () => {
const buildId = readFileSync(join(__dirname, '../.next/BUILD_ID'), 'utf8')

const pathsToCheck = [
`/_next/${buildId}/page/../../../info`,
`/_next/${buildId}/page/../../../info.js`,
`/_next/${buildId}/page/../../../info.json`
]

for (const path of pathsToCheck) {
const data = await renderViaHTTP(appPort, path)
expect(data.includes('cool-version')).toBeFalsy()
}
})
})

describe('X-Powered-By header', () => {
Expand All @@ -148,4 +115,6 @@ describe('Production Usage', () => {
})

dynamicImportTests(context, (p, q) => renderViaHTTP(context.appPort, p, q))

security(context)
})
Loading

0 comments on commit 7afc008

Please sign in to comment.