Repository: tomsoderlund/nextjs-express-mongoose-crudify-boilerplate
Branch: master
Commit: cbc5ba79c372
Files: 15
Total size: 17.3 KB
Directory structure:
gitextract_lwe4j0o4/
├── .gitignore
├── README.md
├── components/
│ ├── KittenItem.js
│ └── PageHead.js
├── config/
│ └── config.js
├── package.json
├── pages/
│ ├── _app.js
│ └── index.js
├── redux/
│ └── reduxApi.js
├── server/
│ ├── api/
│ │ └── kittens.js
│ ├── models/
│ │ └── kitten.js
│ ├── routes.js
│ ├── server.js
│ └── services/
│ └── helpers.js
└── static/
└── app.css
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules/
.next/
================================================
FILE: README.md
================================================
# Next.js (React) + Redux + Express REST API + MongoDB + Mongoose-Crudify boilerplate
_Note: this is my v1 boilerplate for React web apps. See also my [Firebase and React Hooks boilerplate](https://github.com/tomsoderlund/nextjs-pwa-firebase-boilerplate), [GraphQL + Postgres SQL boilerplate](https://github.com/tomsoderlund/nextjs-pwa-graphql-sql-boilerplate), and [Redux + REST + Postgres SQL boilerplate](https://github.com/tomsoderlund/nextjs-sql-rest-api-boilerplate)._
This template is based on [nextjs-express-boilerplate](https://github.com/johhansantana/nextjs-express-boilerplate), but with added [mongoose-crudify](https://github.com/ryo718/mongoose-crudify) and [redux-api](https://github.com/lexich/redux-api).
## Support this project
Did you or your company find `nextjs-express-mongoose-crudify-boilerplate` useful? Please consider giving a small donation, it helps me spend more time on open-source projects:
[](https://ko-fi.com/tomsoderlund)
## Why is this awesome?
This is a great starting point for a any project where you want **React + Redux** (with server-side rendering, powered by [Next.js](https://github.com/zeit/next.js)) as frontend and **Express/MongoDB** as a REST API backend.
_Lightning fast, all JavaScript._
* Simple REST API routes with MongoDB database and `mongoose-crudify`.
* Redux REST support with `redux-api` and `next-redux-wrapper`.
* Flexible client-side routing with `next-routes` (see `server/routes.js`).
* Flexible configuration with `config/config.js` and `.env` file.
* Hot reloading with `nodemon`.
* Testing with Jasmine.
* Code formatting and linting with StandardJS.
* JWT authentication for client-server communication (coming).
## Demo
See [**nextjs-express-mongoose-crudify-boilerplate** running on Heroku here](https://nextjs-express-mongoose.herokuapp.com/).

## Don’t want Redux?
This project now uses Redux and [redux-api](https://github.com/lexich/redux-api). See the [no-redux](https://github.com/tomsoderlund/nextjs-express-mongoose-crudify-boilerplate/tree/no-redux) branch for the (unmaintained) version without Redux.
## How to use
Clone this repository:
git clone https://github.com/tomsoderlund/nextjs-express-mongoose-crudify-boilerplate.git [MY_APP]
Install dependencies:
cd [MY_APP]
yarn # or npm install
Start it by doing the following:
export MONGODB_URI=*your mongodb url* // you can get one for free at https://www.mlab.com/home
yarn dev
In production:
yarn build
yarn start
If you navigate to `http://localhost:3001/` you will see a [Next.js](https://github.com/zeit/next.js) page with a list of kittens (or an empty list if you haven't added one).
You have your API server running at `http://localhost:3001/api/kittens`
## Deploying
### Deploying on Heroku
heroku create [MY_APP]
heroku addons:add mongolab
git push heroku master
### Deploying on Now
See instructions on [nextjs-express-boilerplate](https://github.com/johhansantana/nextjs-express-boilerplate).
================================================
FILE: components/KittenItem.js
================================================
const KittenItem = ({ kitten, index, inProgress, handleUpdate, handleDelete }) => (
)
export default KittenItem
================================================
FILE: components/PageHead.js
================================================
import Head from 'next/head'
const PageHead = ({ title, description }) => (
{title}
)
export default PageHead
================================================
FILE: config/config.js
================================================
const appName = 'nextjs-express-mongoose-crudify-boilerplate'
const databaseName = 'nextjs-express-boilerplate'
const serverPort = process.env.PORT || 3122
const completeConfig = {
default: {
appName,
serverPort,
databaseUrl: process.env.MONGODB_URI || `mongodb://localhost/${databaseName}`,
jsonOptions: {
headers: {
'Content-Type': 'application/json'
}
}
},
development: {
appUrl: `http://localhost:${serverPort}/`
},
production: {
appUrl: `https://nextjs-express-mongoose.herokuapp.com/`
}
}
// Public API
module.exports = {
config: { ...completeConfig.default, ...completeConfig[process.env.NODE_ENV] },
completeConfig
}
================================================
FILE: package.json
================================================
{
"name": "nextjs-express-mongoose-crudify-boilerplate",
"version": "4.0.0",
"description": "Next.js (React) + Redux + Express REST API + Mongoose CRUD boilerplate.",
"main": "server/server.js",
"license": "ISC",
"scripts": {
"test": "echo 'Running Standard.js and Jasmine unit tests...\n' && yarn lint && yarn unit",
"unit": "jasmine",
"lint": "standard",
"fix": "standard --fix",
"dev": "nodemon -w server -w package.json server/server.js",
"build": "next build",
"heroku-postbuild": "next build",
"start": "NODE_ENV=production node server/server.js"
},
"now": {
"name": "nextjs-express-mongoose-crudify-boilerplate",
"alias": "nextjs-express-mongoose-crudify-boilerplate"
},
"engines": {
"node": "^10.13.0",
"yarn": "^1.3.2"
},
"dependencies": {
"body-parser": "^1.15.2",
"dotenv": "^6.2.0",
"express": "^4.14.0",
"glob": "^7.1.2",
"isomorphic-fetch": "^2.2.1",
"isomorphic-unfetch": "^2.0.0",
"lodash": "^4.17.4",
"mongoose": "^4.7.6",
"mongoose-crudify": "^0.2.0",
"next": "^8.0.3",
"next-redux-wrapper": "^3.0.0-alpha.1",
"next-routes": "^1.4.2",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-redux": "^5.0.6",
"redux": "^3.7.2",
"redux-api": "^0.11.1",
"redux-thunk": "^2.2.0"
},
"devDependencies": {
"babel-eslint": "^10.0.1",
"jasmine": "^3.3.1",
"nodemon": "^1.12.1",
"standard": "^12.0.1"
}
}
================================================
FILE: pages/_app.js
================================================
// pages/_app.js
import React from 'react'
import { Provider } from 'react-redux'
import App, { Container } from 'next/app'
import withRedux from 'next-redux-wrapper'
import { makeStore } from '../redux/reduxApi.js'
class MyApp extends App {
static async getInitialProps ({ Component, ctx }) {
return {
pageProps: {
// Call page-level getInitialProps
...(Component.getInitialProps ? await Component.getInitialProps(ctx) : {})
}
}
}
render () {
const { Component, pageProps, store } = this.props
return (
)
}
}
export default withRedux(makeStore, { debug: false })(MyApp)
================================================
FILE: pages/index.js
================================================
import { Component } from 'react'
import reduxApi, { withKittens } from '../redux/reduxApi.js'
import { Link } from '../server/routes.js'
import PageHead from '../components/PageHead'
import KittenItem from '../components/KittenItem'
class IndexPage extends Component {
static async getInitialProps ({ store, isServer, pathname, query }) {
// Get all kittens
const kittens = await store.dispatch(reduxApi.actions.kittens.sync())
return { kittens, query }
}
constructor (props) {
super(props)
this.state = { name: '' }
}
handleChangeInputText (event) {
this.setState({ name: event.target.value })
}
handleAdd (event) {
const { name } = this.state
if (!name) return
const callbackWhenDone = () => this.setState({ name: '', inProgress: false })
this.setState({ inProgress: true })
// Actual data request
const newKitten = { name }
this.props.dispatch(reduxApi.actions.kittens.post({}, { body: JSON.stringify(newKitten) }, callbackWhenDone))
}
handleUpdate (kitten, index, kittenId, event) {
const name = window.prompt('New name?', kitten.name)
if (!name) return
const callbackWhenDone = () => this.setState({ inProgress: false })
this.setState({ inProgress: kittenId })
// Actual data request
const newKitten = { id: kittenId, name }
this.props.dispatch(reduxApi.actions.kittens.put({ id: kittenId }, { body: JSON.stringify(newKitten) }, callbackWhenDone))
}
handleDelete (index, kittenId, event) {
const callbackWhenDone = () => this.setState({ inProgress: false })
this.setState({ inProgress: kittenId })
// Actual data request
this.props.dispatch(reduxApi.actions.kittens.delete({ id: kittenId }, callbackWhenDone))
}
render () {
const { kittens } = this.props// dd
const kittenList = kittens.data
? kittens.data.map((kitten, index) => )
: []
return
Kittens
{kittenList}
Add kitten
Routing
Current page slug: /{this.props.query.slug}
};
}
export default withKittens(IndexPage)
================================================
FILE: redux/reduxApi.js
================================================
import _ from 'lodash'
import fetch from 'isomorphic-fetch'
import reduxApi, { transformers } from 'redux-api'
import adapterFetch from 'redux-api/lib/adapters/fetch'
import { createStore, applyMiddleware, combineReducers } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { connect } from 'react-redux'
const { config } = require('../config/config')
const apiTransformer = function (data, prevData, action) {
const actionMethod = _.get(action, 'request.params.method')
switch (actionMethod) {
case 'POST':
return [...prevData, data]
case 'PUT':
return prevData.map(oldData => oldData._id === data._id ? data : oldData)
case 'DELETE':
return _(prevData).filter(oldData => oldData._id === data._id ? undefined : oldData).compact().value()
default:
return transformers.array.call(this, data, prevData, action)
}
}
// redux-api documentation: https://github.com/lexich/redux-api/blob/master/docs/DOCS.md
const thisReduxApi = reduxApi({
// Simple endpoint description
// oneKitten: '/api/kittens/:id',
// Complex endpoint description
kittens: {
url: '/api/kittens/:id',
crud: true, // Make CRUD actions: https://github.com/lexich/redux-api/blob/master/docs/DOCS.md#crud
// base endpoint options `fetch(url, options)`
options: config.jsonOptions,
// reducer (state, action) {
// console.log('reducer', action);
// return state;
// },
// postfetch: [
// function ({data, actions, dispatch, getState, request}) {
// console.log('postfetch', {data, actions, dispatch, getState, request});
// dispatch(actions.kittens.sync());
// }
// ],
// Reimplement default `transformers.object`
// transformer: transformers.array,
transformer: apiTransformer
}
})
.use('fetch', adapterFetch(fetch))
.use('rootUrl', config.appUrl)
export default thisReduxApi
const createStoreWithThunkMiddleware = applyMiddleware(thunkMiddleware)(createStore)
export const makeStore = (reduxState, enhancer) => createStoreWithThunkMiddleware(combineReducers(thisReduxApi.reducers), reduxState)
// endpointNames: Use reduxApi endpoint names here
const mapStateToProps = (endpointNames, reduxState) => {
let props = {}
for (let i in endpointNames) {
props[endpointNames[i]] = reduxState[endpointNames[i]]
props[`${endpointNames[i]}Actions`] = thisReduxApi.actions[endpointNames[i]]
}
return props
}
export const withReduxEndpoints = (PageComponent, endpointNames) => connect(mapStateToProps.bind(undefined, endpointNames))(PageComponent)
// Define custom endpoints/providers here:
export const withKittens = PageComponent => withReduxEndpoints(PageComponent, ['kittens'])
================================================
FILE: server/api/kittens.js
================================================
'use strict'
const mongooseCrudify = require('mongoose-crudify')
const helpers = require('../services/helpers')
const Kitten = require('../models/kitten')
module.exports = function (server) {
// Docs: https://github.com/ryo718/mongoose-crudify
server.use(
'/api/kittens',
mongooseCrudify({
Model: Kitten,
selectFields: '-__v', // Hide '__v' property
endResponseInAction: false,
// beforeActions: [],
// actions: {}, // list (GET), create (POST), read (GET), update (PUT), delete (DELETE)
afterActions: [
{ middlewares: [helpers.formatResponse] }
]
})
)
}
================================================
FILE: server/models/kitten.js
================================================
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const kittenSchema = new Schema({
name: { type: String, required: true }
})
module.exports = mongoose.model('Kitten', kittenSchema)
================================================
FILE: server/routes.js
================================================
const routes = require('next-routes')
const routesImplementation = routes()
// routesImplementation
// .add([identifier], pattern = /identifier, page = identifier)
// .add('/blog/:slug', 'blogShow')
// .add('showBlogPostRoute', '/blog/:slug', 'blogShow')
routesImplementation.add('/:slug', 'index')
routesImplementation.add('/more/:slug', 'index')
module.exports = routesImplementation
// Usage inside Page.getInitialProps (req = { pathname, asPath, query } = { pathname: '/', asPath: '/about', query: { slug: 'about' } })
================================================
FILE: server/server.js
================================================
require('dotenv').config()
const express = require('express')
const server = express()
const bodyParser = require('body-parser')
const mongoose = require('mongoose')
const glob = require('glob')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const routes = require('./routes')
const routerHandler = routes.getRequestHandler(app)
const { config } = require('../config/config')
app.prepare().then(() => {
// Parse application/x-www-form-urlencoded
server.use(bodyParser.urlencoded({ extended: false }))
// Parse application/json
server.use(bodyParser.json())
// Allows for cross origin domain request:
server.use(function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
next()
})
// MongoDB
mongoose.Promise = Promise
mongoose.connect(config.databaseUrl, { useMongoClient: true })
const db = mongoose.connection
db.on('error', console.error.bind(console, 'connection error:'))
// REST API routes
const rootPath = require('path').join(__dirname, '/..')
glob.sync(rootPath + '/server/api/*.js').forEach(controllerPath => {
if (!controllerPath.includes('.test.js')) require(controllerPath)(server)
})
// Next.js page routes
server.get('*', routerHandler)
// Start server
server.listen(config.serverPort, () => console.log(`${config.appName} running on http://localhost:${config.serverPort}/`))
})
================================================
FILE: server/services/helpers.js
================================================
//
// Name: helpers.js
// Purpose: Library for helper functions
// Creator: Tom Söderlund
//
'use strict'
const _ = require('lodash')
// Since DELETE doesn't return the _id of deleted item by default
module.exports.formatResponse = function (req, res, next) {
if (req.crudify.err) console.error('formatResponse:', _.get(req, 'crudify.err.message'))
return res.json(req.crudify.err || (req.method === 'DELETE' ? req.params : req.crudify.result))
}
================================================
FILE: static/app.css
================================================
* {
box-sizing: border-box;
}
body {
margin: 1em;
font-family: sans-serif;
font-size: 20px;
}
@media only screen and (max-width: 480px) {
input, button {
width: 100%;
}
}
/* Nice & simple: Button - http://codepen.io/tomsoderlund/pen/qqyzqp */
button {
background-color: dodgerblue;
border-radius: 0.2em;
border: none;
box-shadow: 0 0.125em 0.125em rgba(0,0,0, 0.3);
box-sizing: border-box;
color: white;
cursor: pointer;
font-family: inherit;
font-size: inherit;
font-weight: bold;
outline: none;
padding: 0.6em;
margin: 0.2em;
transition: all 0.2s;
}
button:hover:not(:disabled) {
opacity: 0.8;
transition: box-shadow 0s;
}
button:active {
margin-top: 0.3em;
margin-bottom: 0.1em;
box-shadow: 0 0.5px 0.125em rgba(0,0,0, 0.4);
}
button:disabled {
background-color: silver;
}
/* Nice & simple: Input and Dropdown Menu - http://codepen.io/tomsoderlund/pen/GNBbWz */
input,
textarea,
select {
outline: none;
resize: none;
box-shadow: inset 0 0.125em 0.125em rgba(0,0,0, 0.3);
box-sizing: border-box;
background-color: white;
border-radius: 0.2em;
border: 2px solid lightgray;
color: inherit;
font-family: inherit;
font-size: inherit;
padding: 0.6em;
margin: 0.2em;
}
input:hover:not(:disabled),
textarea:hover:not(:disabled),
select:hover:not(:disabled) {
border-color: silver;
}
input:focus,
textarea:focus,
select:focus {
border-color: darkgray;
}
input:disabled,
textarea:disabled,
select:disabled {
background-color: whitesmoke;
}