├── .babelrc ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── css │ └── index.less └── js │ ├── index.js │ └── log.js ├── server ├── components │ └── app.js ├── index.js ├── routes │ └── index.js └── views │ └── template.js ├── webpack.config.base.js ├── webpack.config.dev.js └── webpack.config.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ['env', 'react'], 3 | "plugins": [ 4 | ["module-resolver", { 5 | "root": ["./"], 6 | }] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-react-ssr-example 2 | A starter for a project using ExpressJs and ReactJs with server side rendering. 3 | 4 | Starting a development environment: 5 | 6 | npm run start:dev 7 | 8 | Starting a production environment: 9 | 10 | npm run start:prod 11 | 12 | We use webpack (that uses internally babel as a loader) to transpile the front js and css files. We also use babel cli to transpile the server js files. The result will be in the "dist" folder which is then used by nodemon (dev) or pm2 (prod) to launch the server. Changes in the css or js files are automatically loaded. Just refresh the browser and changes will appear. 13 | 14 | **Obs:** 15 | 16 | In production, the css is in a separated file (bundle.css). It is also minified and the vendor prefixes (webkit, ms...) are added. 17 | 18 | In development, the css is directly inserted into the html. It's not minified and no vendor prefixes are added. 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-react-ssr-starter", 3 | "version": "1.0.0", 4 | "description": "A starter for a project using ExpressJs and ReactJs with server side rendering.", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev:build:back": "babel ./server -d dist --watch", 8 | "prod:build:back": "babel ./server -d dist", 9 | "dev:build:front": "cross-env NODE_ENV=dev webpack --config webpack.config.dev.js", 10 | "prod:build:front": "webpack --config webpack.config.prod.js", 11 | "dev:launch:server": "nodemon --inspect=9230 dist/index.js", 12 | "prod:launch:server": "pm2 start dist/index.js", 13 | "start:dev": "npm-run-all --parallel dev:build:back dev:build:front dev:launch:server", 14 | "start:prod": "npm run prod:build:back && npm run prod:build:front && npm run prod:launch:server" 15 | }, 16 | "author": "Ana Luiza Cicconi", 17 | "license": "ISC", 18 | "dependencies": { 19 | "express": "^4.16.2", 20 | "path": "^0.12.7", 21 | "pm2": "^2.7.2", 22 | "react": "^16.1.1", 23 | "react-dom": "^16.1.1" 24 | }, 25 | "devDependencies": { 26 | "autoprefixer": "^7.1.6", 27 | "babel-cli": "^6.26.0", 28 | "babel-loader": "^7.1.2", 29 | "babel-plugin-module-resolver": "^3.0.0", 30 | "babel-preset-env": "^1.6.1", 31 | "babel-preset-react": "^6.24.1", 32 | "cross-env": "^5.1.1", 33 | "css-loader": "^0.28.7", 34 | "extract-text-webpack-plugin": "^3.0.2", 35 | "less": "^2.7.3", 36 | "less-loader": "^4.0.5", 37 | "nodemon": "^1.12.1", 38 | "npm-run-all": "^4.1.2", 39 | "postcss-loader": "^2.0.8", 40 | "style-loader": "^0.19.0", 41 | "uglifyjs-webpack-plugin": "^1.0.1", 42 | "webpack": "^3.8.1", 43 | "webpack-merge": "^4.1.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/css/index.less: -------------------------------------------------------------------------------- 1 | @background: green; 2 | 3 | body { 4 | background-color: @background; 5 | transform: translateX(20px); 6 | } 7 | -------------------------------------------------------------------------------- /public/js/index.js: -------------------------------------------------------------------------------- 1 | import { log1, log2} from './log.js'; 2 | 3 | import css from '../css/index.less'; 4 | 5 | let test = 'test es2015 working'; 6 | 7 | function testing() { 8 | // test that the css was loaded thanks to css-loader and style-loader 9 | console.log(css); 10 | 11 | // test that es2015 features were transpiled thanks to babel-preset-env 12 | console.log(test); 13 | } 14 | 15 | testing(); 16 | 17 | // test that we are able to use functions from another file 18 | log1(); 19 | log2(); 20 | -------------------------------------------------------------------------------- /public/js/log.js: -------------------------------------------------------------------------------- 1 | export function log1 () { 2 | console.log('log 1'); 3 | } 4 | 5 | export function log2 () { 6 | console.log('log 2'); 7 | } 8 | -------------------------------------------------------------------------------- /server/components/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class App extends React.Component { 4 | render() { 5 | return

SSR Hello World

; 6 | } 7 | } 8 | 9 | export default App; 10 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import index from './routes/index'; 4 | 5 | const app = express(); 6 | 7 | // set static folder for generated css and front js files 8 | app.use('/public', express.static(path.join(__dirname, 'public'))); 9 | 10 | // set routes 11 | app.use('/', index); 12 | 13 | // catch 404 and forward to error handler 14 | /*app.use(function(req, res, next) { 15 | var err = new Error('Not Found'); 16 | err.status = 404; 17 | next(err); 18 | });*/ 19 | 20 | // error handler 21 | /*app.use(function(err, req, res, next) { 22 | // set locals, only providing error in development 23 | res.locals.message = err.message; 24 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 25 | 26 | // render the error page 27 | res.status(err.status || 500); 28 | res.render('error'); 29 | });*/ 30 | 31 | app.listen(3000, function () { 32 | console.log('Example app listening on port 3000!'); 33 | }); 34 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import React from 'react'; 3 | import { renderToString } from 'react-dom/server'; 4 | import App from 'server/components/app'; 5 | import template from 'server/views/template'; 6 | 7 | let router = express.Router(); 8 | 9 | /* GET home page. */ 10 | router.get('/', function(req, res, next) { 11 | 12 | const appString = renderToString(); 13 | 14 | res.send(template({ 15 | body: appString, 16 | title: 'SSR Hello World' 17 | })); 18 | }); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /server/views/template.js: -------------------------------------------------------------------------------- 1 | export default ({ body, title }) => { 2 | return ` 3 | 4 | 5 | 6 | ${title} 7 | 8 | 9 | 10 | 11 |
${body}
12 | 13 | 14 | 15 | `; 16 | }; 17 | -------------------------------------------------------------------------------- /webpack.config.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 3 | const dev = process.env.NODE_ENV === 'dev'; 4 | 5 | let cssLoaders = [ 6 | { loader: 'css-loader', options: { importLoaders: 1, minimize: !dev } } 7 | ]; 8 | 9 | let config = { 10 | entry: { 11 | bundle: './public/js/index.js' 12 | }, 13 | output: { 14 | path: path.resolve(__dirname, 'dist/public'), 15 | filename: 'bundle.js' 16 | }, 17 | module: { 18 | rules: [ 19 | // the babel-loader converts new js features into old ones 20 | { 21 | test: /\.(js|jsx)$/, 22 | exclude: /node_modules/, 23 | use: ['babel-loader'] 24 | } 25 | ] 26 | }, 27 | plugins: [ 28 | // it's better to have the css in the middle of the html during development so we can use the hot loading 29 | new ExtractTextPlugin({ 30 | filename: '[name].css', 31 | disable: dev 32 | }) 33 | ] 34 | }; 35 | 36 | module.exports = { 37 | config, 38 | cssLoaders 39 | } 40 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 3 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 4 | const merge = require('webpack-merge'); 5 | const webpack = require('webpack'); 6 | 7 | const baseConfig = require('./webpack.config.base').config; 8 | const cssLoaders = require('./webpack.config.base').cssLoaders; 9 | 10 | const config = merge(baseConfig, { 11 | watch: true, 12 | // the devtool enable us to see the original file when we debug a line and not the bundle.js 13 | devtool: 'cheap-module-eval-source-map', 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(css|less)$/, 18 | use: ExtractTextPlugin.extract({ 19 | fallback: "style-loader", 20 | use: [...cssLoaders, 'less-loader'] 21 | }) 22 | } 23 | ] 24 | }, 25 | plugins: [ 26 | new webpack.HotModuleReplacementPlugin() 27 | ] 28 | }); 29 | 30 | module.exports = config; 31 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 3 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 4 | const merge = require('webpack-merge'); 5 | 6 | const baseConfig = require('./webpack.config.base').config; 7 | let cssLoaders = require('./webpack.config.base').cssLoaders; 8 | 9 | cssLoaders = [...cssLoaders, { 10 | loader: 'postcss-loader', 11 | options: { 12 | // the autoprefixer adds the vendor prefixes to css (ex: webkit, ms) 13 | plugins: (loader) => [ 14 | require('autoprefixer')({ 15 | browsers: ['last 2 versions', 'ie > 8'] 16 | }) 17 | ] 18 | } 19 | }] 20 | 21 | 22 | const config = merge(baseConfig, { 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(css|less)$/, 27 | use: ExtractTextPlugin.extract({ 28 | fallback: "style-loader", 29 | use: [...cssLoaders, 'less-loader'] 30 | }) 31 | } 32 | ] 33 | }, 34 | plugins: [ 35 | new UglifyJsPlugin({ 36 | sourceMap: false 37 | }) 38 | ] 39 | }); 40 | 41 | module.exports = config; 42 | --------------------------------------------------------------------------------