Full stack js boilerplate
CONTENTS:
The main purpose of this boiler plate is to shorten scaffolding for every new project with ready to use universal solution bundle. It can be easily extended and customized. It contains implementation of basic features such as:
- Server:
- Multi environment configuration
- Api route CRUD helpers and tests
- Universal render and component state prefetch
- Api request logging middleware
- Client:
- 'Login' and 'Sign up' of users
- Private and public section access permission check
- Modal popup demo
- Async api call middleware
Base technology stack consist of:
- node@0.12.7
babel@5.8.23 | ES5/6/7 polyfill |
bootstrap@3.3.5 | facebook UI/UX framework |
bunyan@1.4.0 | full featured JSON logging |
express.js@4.13.1 | http server |
konphyg@1.4.0 | multi environment json based app config |
mocha@2.2.5 + chai@2.3.0 | testing framework |
mongoose@4.1.6 | schema based MongoDB interface |
react@0.13.3 | client side virtual DOM view manipulation and rendering |
react-bootstrap@0.24.3 | easy rendering of twitter-bootstrap elements in react style |
react-redux@1.0.1 | binding redux state and actions into react components |
react-router@1.0.0-beta3 | universal router |
redux@1.0.1 | flux implementation with single app state and reducers |
redux-devtools@2.1.0 | DX tool |
webpack@1.12.1 | source code bundle maker |
npm i
Before running server you need to create local git ignored secrets.json
npm run init
Build dev bundle and run dev server:
npm run build
npm start
Build prod bundle and run prod server:
npm run build-prod
npm run start-prop
pending
Express app is wrapped in basic http server in order to be easy extended. E.g. to use sockets.
On the top of entry point server.js
babel/register
is included for server side render of client es6 modules in redux-handler.js. This gave possibility to use es6 all over the server app but it was not the main purpose of this boilerplate. And now with Node.js v4 it(ES6) can be used without babel
in general. But this is another story.
For multi environment configuration used konphyg. The key feature of this approach is to put only difference in new env config file:
config/server.json
{
"host": "127.0.0.1",
"port": "3020",
"uploadsDir": "uploads/",
"api": {
"mountPoint":"/api"
}
}
config/server.test.json
{
"port": "3021"
}
Usually you will need to store different API keys somewhere. And secrets.json
is used for this. It's git ignored by default and nothing will be committed to the repo. You need just create and update this file manually on every environment where you plan to use this app.
On the top of bunyan
JSON logging module bunyan-format
is used to make logs human friendly on dev environment. You can adjust log serializers in logger.js
To make api endpoint creation easy and really fast buildCRUD.js helper is used. It has predefined set of mapped actions and automatically apply it for defined model:
var apiHelpers = requireTree('../lib/api-helpers')
var User = $require('models/user')
module.exports = function (router) {
router.use('/users', apiHelpers.buildCRUD(User))
}
In this example we've defined CRUD actions for User
resource mounted on /users
of router
mount point.
Also we can disable any action and define pre-
& post-
filter for actions.
var apiHelpers = requireTree('../lib/api-helpers')
var User = $require('models/user')
module.exports = function (router) {
router.use('/users', apiHelpers.buildCRUD(User, {
actions: {
'pre-list': apiHelpers.allowLogged,
'pre-retrieve': apiHelpers.allowLogged,
'pre-patch': apiHelpers.allowLogged,
'pre-remove': apiHelpers.allowLogged,
update: false
}
}))
}
You can use array of functions in pre-
and post-
. Complete set of predefined mapping is:
create
->POST
list
->GET
retrieve
->GET /:objectId
retrieve-set
->GET /$idListRegExp
:/([0-9a-fA-F;]{25,256})/
reg exp used to determine object id set in query and serve it in one request and response. Usefull forbackbone
style client model organisation.update
->PUT /:objectId
remove
->DELETE /:objectId
patch
->PATCH /:objectId
Note: objectId
when defined in action mapping will be availabale in req.params.objectId
To populate some fields of result populate
option of buildCRUD
is used:
var apiHelpers = requireTree('../lib/api-helpers')
var User = $require('models/user')
module.exports = function (router) {
router.use('/users', apiHelpers.buildCRUD(User, {
actions: {
update: false
},
populate: ['prop1', 'prop2']
}))
}
Binded JWT middleware automatically transforms request token to req.session.userId
and then allowLogged
helper check for proper user or respond with not-authorized error.
In this way it can be used in pre-
hook for any non-public api's.
Handlebars used as main template engine, but it can be easily replaced with what you want.
Main html meta tags are set via redux-handler.js from package.json
Every incoming request to the server passing through all the middlewares and routers. And when target route handler is defined and does his job at the end it just call next()
. Or if some error occurs then handler will call next(err)
. In such a way we put at the very end of middleware chain 3 main responders:
- redux-handler.js - it dedicated to catch all
/app
request and serve it ashtml
page with prefetched state and layout. All client side javascript loaded by reference inhead
. So it very usefull for indexing by search engines.
- err-handlers.js - will catch all
next(err)
calls. It mean all catched error will be processed to the client with error message. - send-response.js - will send to client content of
res.body
as JSON withres.code
status.
This approach give us ability to process response body trhough couple of hooks. And have only single exit
-point from server.
To simplify common error repsonses usage there is a couple of predefined error responders which are just wrapper for specific kind of error message and status:
For exmaple let's take a look on allowLogged
api helper:
var $require = require(process.cwd() + '/lib/require')
var User = $require('models/user')
var errResNotFound = require('../api-err-responders/resource-not-found')
var errResNotAuthorized = require('../api-err-responders/not-authorized')
module.exports = function allowLogged (req, res, next) {
if (!req.session.userId) {
return next(errResNotAuthorized())
}
if (!req.user) {
User.findById(req.session.userId, function (err, user) {
if (err) return next(err)
if (!user) return next(errResNotFound(req.session.userId, 'User'))
req.user = user
next()
})
} else {
next()
}
}
When user is not authorized we use not-authorized
and when there is no such user found - resource-not-found
error responder.
Redux app architecture is clear and extensible enough, so you can find all its part in /src/front-end/js
with small difference described below.
Constants lives in single file to not to produce a lot of small files. Custom keyMirror
function is applyed to key tree - when there is already a value set for key - it ignored. This is convenient for api call state constants:
{
FETCH_APP_STATE: {
REQUEST: 'FETCH_APP_STATE_REQUEST',
SUCCESS: 'FETCH_APP_STATE_SUCCESS',
ERROR: 'FETCH_APP_STATE_ERROR'
}
}
Routes resides in single file routes.js
where all layout components imported. Universal router resides in router.js
and here is router started logic is applayed in such a way that this module can be imported both in client start script index.js
and in server redux-handler.js
.
In most cases your app will need some server data to have in app state before to render. So you can write static method fetchState
for you class and it will be called on router start. It's very usefull for server render - you can fetch some data and render in one request.
// inside of App class definition
static fetchState (store, params, query) {
if (isFetched(store.getState())) {
return Promise.resolve()
} else {
return Promise.all([store.dispatch(AppActions.fetchState(params, query))])
}
}
In terms of React DOM architecture we have couple of basic containers to wrap in default logic:
containers/App.js
- application itself. The root app componentcontainers/ModalsContainer.js
- container to manage popup windows.containers/RouterContainer.js
- wrapper for router to bind auth check logic.
App store is constructed from middlewares and reducers in store.js
. Please note that global consts __CLIENT__
and other are defined for server and client rendering in different places. When script is running in browser - they set via webpack bundle. And when on server - as GLOBAL
constants.
There is also simple middleware to make API call from actions in async manner with promises. To make api call you need to define special actons type:
{
FETCH_APP_STATE: {
REQUEST: 'FETCH_APP_STATE_REQUEST',
SUCCESS: 'FETCH_APP_STATE_SUCCESS',
ERROR: 'FETCH_APP_STATE_ERROR'
}
}
And dispatch FETCH_APP_STATE
with API url:
store.dispatch({
type: FETCH_APP_STATE,
payload: '/users/current'
})
Middleware then will dispatch REQUEST
action with no payload and return promise so you can wait for finish of request execution. Then SUCCESS
or ERROR
will be dispatched with corresponding payload. And you need not to forget to implement reducers for all this 3 actions: REQUEST
, SUCCESS
, ERROR
.
I was using a tons of different web sources and examples, but big redux part of this repo is based on react-redux-universal-hot-example example.
Because E
xpress M
ongoose MO
ngo F
lux RE
act T
witterbootstrap =8)