├── .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 |
19 | { this.props.shouts.map((s, i) => - {s}
) }
20 |
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 |
--------------------------------------------------------------------------------