├── .nvmrc ├── .gitignore ├── src ├── frontend │ ├── reducers │ │ ├── index.js │ │ └── shouts.js │ ├── constants │ │ └── index.js │ ├── components │ │ ├── index.js │ │ └── Application.js │ ├── index.template.html │ ├── actions │ │ └── index.js │ ├── index.js │ └── Root.js └── server │ └── index.js ├── .babelrc ├── package.json ├── webpack.config.js ├── gulpfile.babel.js └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ -------------------------------------------------------------------------------- /src/frontend/reducers/index.js: -------------------------------------------------------------------------------- 1 | export { default as shouts } from './shouts'; 2 | -------------------------------------------------------------------------------- /src/frontend/constants/index.js: -------------------------------------------------------------------------------- 1 | export const RECEIVED_SHOUTS = 'RECEIVED_SHOUTS'; 2 | -------------------------------------------------------------------------------- /src/frontend/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Application } from './Application'; 2 | -------------------------------------------------------------------------------- /src/frontend/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {%=o.htmlWebpackPlugin.options.title || 'Skele' %} 5 | 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /src/frontend/actions/index.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; //polyfill 2 | import { RECEIVED_SHOUTS } from '../constants'; 3 | 4 | export function fetchShouts() { 5 | return dispatch => { 6 | fetch('/api') 7 | .then(resp => resp.json()) 8 | .then(json => dispatch({ 9 | type: RECEIVED_SHOUTS, 10 | shouts: json.shouts 11 | })); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/frontend/index.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import Root from './Root'; 3 | import { createHistory } from 'history'; 4 | 5 | // Import required so that React is available even 6 | // though it is not used in this file 7 | import React from 'react'; 8 | 9 | ReactDOM.render( 10 | , 11 | document.getElementById('container') 12 | ); -------------------------------------------------------------------------------- /src/frontend/reducers/shouts.js: -------------------------------------------------------------------------------- 1 | import { RECEIVED_SHOUTS } from '../constants'; 2 | 3 | const initialState = []; 4 | 5 | const actionsMap = { 6 | [RECEIVED_SHOUTS]: (state, action) => action.shouts 7 | }; 8 | 9 | export default function shouts(state = initialState, action) { 10 | const fn = actionsMap[action.type]; 11 | if (!fn) { 12 | return state; 13 | } 14 | return fn(state, action); 15 | } 16 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 1, 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | "react-transform" 7 | ], 8 | "extra": { 9 | "react-transform": { 10 | "transforms": [{ 11 | "transform": "react-transform-hmr", 12 | "imports": ["react"], 13 | "locals": ["module"] 14 | }, { 15 | "transform": "react-transform-catch-errors", 16 | "imports": ["react", "redbox-react"] 17 | }] 18 | } 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/frontend/components/Application.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { fetchShouts } from '../actions'; 3 | import { connect } from 'react-redux'; 4 | 5 | class Application extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | componentWillMount() { 11 | this.props.dispatch(fetchShouts()); 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 |

Shouts

18 | 21 |
22 | ); 23 | } 24 | } 25 | 26 | export default connect(state => ({ shouts: state.shouts }))(Application); -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const app = express(); 4 | 5 | app.get('/api', (req, res) => { 6 | res.json({ 7 | shouts: [ 8 | 'Hello World!', 9 | 'This is React and Webpack...', 10 | 'They make development fun', 11 | 'Another shout' 12 | ] 13 | }); 14 | }); 15 | 16 | app.get('/api/test', (req, res) => { 17 | res.json({ 18 | hello: "world" 19 | }); 20 | }); 21 | 22 | app.post('/api/test/test', (req, res) => { 23 | res.json({ 24 | hello: "world'" 25 | }); 26 | }); 27 | 28 | app.listen(8080, function(err) { 29 | if (err) 30 | return console.log(err); 31 | console.log('running on localhost:8080'); 32 | }); -------------------------------------------------------------------------------- /src/frontend/Root.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Router, Route } from 'react-router'; 3 | import { Provider } from 'react-redux'; 4 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 5 | import thunk from 'redux-thunk'; 6 | 7 | import { Application } from './components'; 8 | import * as reducers from './reducers'; 9 | 10 | const reducer = combineReducers(reducers); 11 | const finalCreateStore = applyMiddleware(thunk)(createStore); 12 | const store = finalCreateStore(reducer); 13 | 14 | export default class Root extends React.Component { 15 | render() { 16 | const { history } = this.props; 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | } 26 | Root.propTypes = { 27 | history: PropTypes.object.isRequired 28 | }; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-fullstack-skeleton", 3 | "version": "0.0.1", 4 | "description": "A minimal React fullstack skeleton featuring hot reloading and a backend api server.", 5 | "main": "gulpfile.babel.js", 6 | "dependencies": { 7 | "history": "^1.12.3", 8 | "react": "^0.14.0", 9 | "react-dom": "^0.14.0", 10 | "react-redux": "^3.1.0", 11 | "react-router": "^1.0.0-rc1", 12 | "redux": "^3.0.2", 13 | "redux-thunk": "^1.0.0", 14 | "whatwg-fetch": "^0.9.0" 15 | }, 16 | "devDependencies": { 17 | "babel": "^5.8.23", 18 | "babel-core": "^5.8.25", 19 | "babel-loader": "^5.3.2", 20 | "babel-plugin-react-transform": "^1.1.1", 21 | "css-loader": "^0.19.0", 22 | "express": "^4.13.3", 23 | "gulp": "^3.9.0", 24 | "html-webpack-plugin": "^1.6.1", 25 | "nodemon": "^1.7.1", 26 | "react-hot-loader": "^1.3.0", 27 | "react-transform-catch-errors": "^1.0.0", 28 | "react-transform-hmr": "^1.0.1", 29 | "redbox-react": "^1.1.1", 30 | "request": "^2.64.0", 31 | "sass-loader": "^3.0.0", 32 | "source-map-support": "^0.3.2", 33 | "style-loader": "^0.12.4", 34 | "webpack": "^1.12.2", 35 | "webpack-dev-middleware": "^1.2.0", 36 | "webpack-dev-server": "^1.12.0", 37 | "webpack-hot-middleware": "^2.4.1" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/fortruce/react-fullstack-skeleton.git" 42 | }, 43 | "keywords": [ 44 | "react", 45 | "fullstack", 46 | "skeleton", 47 | "hot", 48 | "reload", 49 | "webpack" 50 | ], 51 | "author": "fortruce", 52 | "license": "ISC", 53 | "bugs": { 54 | "url": "https://github.com/fortruce/react-fullstack-skeleton/issues" 55 | }, 56 | "homepage": "https://github.com/fortruce/react-fullstack-skeleton" 57 | } 58 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | TODO: 3 | * add a production flag that disables debug/sourcemaps and minifies 4 | */ 5 | 6 | var webpack = require('webpack'); 7 | var path = require('path'); 8 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 9 | 10 | var frontendConfig = { 11 | entry: [ 12 | 'webpack-hot-middleware/client', 13 | './src/frontend/index.js' 14 | ], 15 | 16 | output: { 17 | filename: 'bundle.js', 18 | path: path.join(__dirname, 'build', 'public') 19 | }, 20 | 21 | devtool: 'sourcemap', 22 | 23 | plugins: [ 24 | new webpack.HotModuleReplacementPlugin(), 25 | new webpack.NoErrorsPlugin(), 26 | new HtmlWebpackPlugin({ 27 | title: 'Skele', 28 | filename: 'index.html', 29 | template: 'src/frontend/index.template.html', 30 | inject: true 31 | }) 32 | ], 33 | 34 | module: { 35 | loaders: [ 36 | { 37 | test: /\.js$/, 38 | include: path.join(__dirname, 'src', 'frontend'), 39 | loaders: ['babel'] 40 | }, 41 | { 42 | test: /\.scss$/, 43 | include: path.join(__dirname, 'src', 'frontend', 'scss'), 44 | loaders: ['style', 'css', 'sass'] 45 | } 46 | ] 47 | } 48 | }; 49 | 50 | var serverConfig = { 51 | entry: './src/server/index.js', 52 | output: { 53 | path: path.join(__dirname, 'build'), 54 | filename: 'server.js', 55 | libraryTarget: 'commonjs2' 56 | }, 57 | 58 | devtool: 'sourcemap', 59 | 60 | target: 'node', 61 | // do not include polyfills or mocks for node stuff 62 | node: { 63 | console: false, 64 | global: false, 65 | process: false, 66 | Buffer: false, 67 | __filename: false, 68 | __dirname: false 69 | }, 70 | // all non-relative modules are external 71 | // abc -> require('abc') 72 | externals: /^[a-z\-0-9]+$/, 73 | 74 | plugins: [ 75 | // enable source-map-support by installing at the head of every chunk 76 | new webpack.BannerPlugin('require("source-map-support").install();', 77 | {raw: true, entryOnly: false}) 78 | ], 79 | 80 | module: { 81 | loaders: [ 82 | { 83 | // transpile all .js files using babel 84 | test: /\.js$/, 85 | exclude: /node_modules/, 86 | loader: 'babel' 87 | } 88 | ] 89 | } 90 | }; 91 | 92 | module.exports = [frontendConfig, serverConfig]; -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import webpack from 'webpack'; 3 | import WebpackHotMiddleware from 'webpack-hot-middleware'; 4 | import WebpackDevMiddleware from 'webpack-dev-middleware'; 5 | import express from 'express'; 6 | import nodemon from 'nodemon'; 7 | import path from 'path'; 8 | import request from 'request'; 9 | 10 | import configs from './webpack.config'; 11 | const [ frontendConfig, backendConfig ] = configs; 12 | 13 | gulp.task('dev', () => { 14 | const compiler = webpack(frontendConfig); 15 | 16 | // const server = new WebpackDevServer(compiler, { 17 | // contentBase: path.join(__dirname, 'build', 'public'), 18 | // historyApiFallback: true, 19 | // hot: true, 20 | // proxy: { 21 | // '*': 'http://localhost:8080' 22 | // } 23 | // }); 24 | 25 | const server = express(); 26 | 27 | // proxy requests to api 28 | server.use('/api*', (req, res) => { 29 | request({ 30 | // use req.originalUrl instead of req.path since mount point is removed 31 | // from req.path (ie: '/api*' will be removed from req.path) 32 | url: 'http://localhost:8080' + req.originalUrl, 33 | qs: req.query, 34 | method: req.method.toUpperCase() 35 | }).pipe(res); 36 | }); 37 | 38 | var webpackDevMiddleware = WebpackDevMiddleware(compiler); 39 | 40 | server.use(webpackDevMiddleware); 41 | server.use(WebpackHotMiddleware(compiler)); 42 | 43 | server.get('*', function(req, res) { 44 | req.url = '/'; 45 | webpackDevMiddleware(req, res, ()=>{}); 46 | }); 47 | 48 | server.listen(3000, 'localhost', (err) => { 49 | if (err) 50 | return console.log(err); 51 | console.log('webpack-dev-server listening on localhost:3000'); 52 | }); 53 | }); 54 | 55 | gulp.task('backend-watch', () => { 56 | webpack(backendConfig).watch(100, (err) => { 57 | if (err) 58 | return console.log(err); 59 | nodemon.restart(); 60 | }); 61 | }); 62 | 63 | gulp.task('server', ['backend-watch'], () => { 64 | nodemon({ 65 | execMap: { 66 | js: 'node' 67 | }, 68 | script: path.join(__dirname, 'build', 'server.js'), 69 | // do not watch any directory/files to refresh 70 | // all refreshes should be manual 71 | watch: ['foo/'], 72 | ext: 'noop', 73 | ignore: ['*'] 74 | }).on('restart', () => { 75 | console.log('nodemon: restart'); 76 | }); 77 | }); 78 | 79 | gulp.task('default', ['dev', 'server']); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React Fullstack Skeleton 2 | ======================== 3 | 4 | This skeleton project is meant to scaffold a typical fullstack React application. 5 | The skeleton uses webpack and gulp to manage the build and provide a great 6 | development experience. The frontend stack is React, react-router, and 7 | Redux. All React changes are automatically hot reloaded 8 | using [react-hot-loader][1]. Also, the backend server is automatically 9 | restarted upon any changes using [nodemon][2]. 10 | 11 | Both the server and frontend code are built and transpiled using webpack, while 12 | gulp is used primarily to start the webpack-dev-server and nodemon. 13 | 14 | ## Directory Structure 15 | 16 | ``` 17 | build/ // webpack build output 18 | public/ // publicly served assets 19 | index.html 20 | bundle.js // frontend bundle built w/ webpack 21 | server.js // backend server built w/ webpack 22 | src/ 23 | frontend/ 24 | components/ // React components 25 | reducers/ // Redux reducers 26 | actions/ // Redux action creators 27 | constants/ // Constants 28 | Root.js // Root component defining Routes 29 | index.js // React.render Root component 30 | server/ 31 | index.js 32 | gulpfile.babel.js 33 | webpack.config.js 34 | ``` 35 | 36 | ## Typical Usage 37 | 38 | This skeleton was designed with typical use case of having a backend api serve 39 | a React SPA. The skeleton automatically proxies all requests to `/api` thru 40 | the webpack-dev-server to the backend server. 41 | 42 | The frontend is automatically hot reloaded whenever you save a file. See 43 | [react-hot-loader][1] for more details on how this works. It enables you to 44 | immediately see changes in React components without losing application state 45 | or having to reload your page! 46 | 47 | The backend server is automatically restarted whenever you save a file. 48 | If, for example, you modify the output of an api endpoint that your frontend 49 | is displaying, then you will have to refresh your page to pull from the new 50 | backend server (unless you are polling your backend already); however, you 51 | are saved from having to stop/restart your backend server manually. 52 | 53 | ## Improvements 54 | 55 | The following improvements need to be made: 56 | 57 | * Add a production build flag that removes source maps and minifies js/html. 58 | * Add loaders to support SASS and introduce a base stylesheet as an example. 59 | 60 | I welcome pull requests, but I am trying to keep this skeleton relatively minimal. 61 | 62 | [1]: http://gaearon.github.io/react-hot-loader/ 63 | [2]: http://nodemon.io/ 64 | --------------------------------------------------------------------------------