[
  {
    "path": ".gitignore",
    "content": "node_modules/\n.next/"
  },
  {
    "path": "README.md",
    "content": "# Next.js (React) + Redux + Express REST API + MongoDB + Mongoose-Crudify boilerplate\n\n_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)._\n\nThis 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).\n\n## Support this project\n\nDid 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:\n\n[![Support Tom on Ko-Fi.com](https://www.tomsoderlund.com/ko-fi_tomsoderlund_50.png)](https://ko-fi.com/tomsoderlund)\n\n## Why is this awesome?\n\nThis 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.\n_Lightning fast, all JavaScript._\n\n* Simple REST API routes with MongoDB database and `mongoose-crudify`.\n* Redux REST support with `redux-api` and `next-redux-wrapper`.\n* Flexible client-side routing with `next-routes` (see `server/routes.js`).\n* Flexible configuration with `config/config.js` and `.env` file.\n* Hot reloading with `nodemon`.\n* Testing with Jasmine.\n* Code formatting and linting with StandardJS.\n* JWT authentication for client-server communication (coming).\n\n## Demo\n\nSee [**nextjs-express-mongoose-crudify-boilerplate** running on Heroku here](https://nextjs-express-mongoose.herokuapp.com/).\n\n![nextjs-express-mongoose-crudify-boilerplate demo on Heroku](docs/kittens-demo.gif)\n\n## Don’t want Redux?\n\nThis 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.\n\n## How to use\n\nClone this repository:\n\n\tgit clone https://github.com/tomsoderlund/nextjs-express-mongoose-crudify-boilerplate.git [MY_APP]\n\nInstall dependencies:\n\n\tcd [MY_APP]\n\tyarn  # or npm install\n\nStart it by doing the following:\n\n\texport MONGODB_URI=*your mongodb url* // you can get one for free at https://www.mlab.com/home\n\tyarn dev\n\nIn production:\n\n\tyarn build\n\tyarn start\n\nIf 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).\n\nYou have your API server running at `http://localhost:3001/api/kittens`\n\n\n## Deploying\n\n### Deploying on Heroku\n\n\theroku create [MY_APP]\n\theroku addons:add mongolab\n\tgit push heroku master\n\n### Deploying on Now\n\nSee instructions on [nextjs-express-boilerplate](https://github.com/johhansantana/nextjs-express-boilerplate).\n"
  },
  {
    "path": "components/KittenItem.js",
    "content": "const KittenItem = ({ kitten, index, inProgress, handleUpdate, handleDelete }) => (\n  <div className={inProgress === kitten._id ? 'inProgress' : ''}>\n    {kitten.name}\n    <a className='update' onClick={handleUpdate.bind(this, index, kitten._id)}>Update</a>\n    <a className='delete' onClick={handleDelete.bind(this, index, kitten._id)}>Delete</a>\n    <style jsx>{`\n      a {\n        margin-left: 0.5em;\n        cursor: pointer;\n        font-size: 0.6em;\n        text-transform: uppercase;\n      }\n      a.update {\n        color: lime;\n      }\n      a.delete {\n        color: tomato;\n      }\n      .inProgress {\n        opacity: 0.3;\n      }\n    `}</style>\n  </div>\n)\nexport default KittenItem\n"
  },
  {
    "path": "components/PageHead.js",
    "content": "import Head from 'next/head'\n\nconst PageHead = ({ title, description }) => (\n  <Head>\n    <title>{title}</title>\n    <meta name='description' content={description} />\n    <meta charSet='utf-8' />\n    <meta httpEquiv='content-language' content='en' />\n    <meta name='viewport' content='initial-scale=1.0, width=device-width' />\n    <link rel='stylesheet' href='/static/app.css' />\n  </Head>\n)\nexport default PageHead\n"
  },
  {
    "path": "config/config.js",
    "content": "const appName = 'nextjs-express-mongoose-crudify-boilerplate'\nconst databaseName = 'nextjs-express-boilerplate'\nconst serverPort = process.env.PORT || 3122\n\nconst completeConfig = {\n\n  default: {\n    appName,\n    serverPort,\n    databaseUrl: process.env.MONGODB_URI || `mongodb://localhost/${databaseName}`,\n    jsonOptions: {\n      headers: {\n        'Content-Type': 'application/json'\n      }\n    }\n  },\n\n  development: {\n    appUrl: `http://localhost:${serverPort}/`\n  },\n\n  production: {\n    appUrl: `https://nextjs-express-mongoose.herokuapp.com/`\n  }\n\n}\n\n// Public API\nmodule.exports = {\n  config: { ...completeConfig.default, ...completeConfig[process.env.NODE_ENV] },\n  completeConfig\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"nextjs-express-mongoose-crudify-boilerplate\",\n\t\"version\": \"4.0.0\",\n\t\"description\": \"Next.js (React) + Redux + Express REST API + Mongoose CRUD boilerplate.\",\n\t\"main\": \"server/server.js\",\n\t\"license\": \"ISC\",\n\t\"scripts\": {\n\t\t\"test\": \"echo 'Running Standard.js and Jasmine unit tests...\\n' && yarn lint && yarn unit\",\n\t\t\"unit\": \"jasmine\",\n\t\t\"lint\": \"standard\",\n\t\t\"fix\": \"standard --fix\",\n\t\t\"dev\": \"nodemon -w server -w package.json server/server.js\",\n\t\t\"build\": \"next build\",\n\t\t\"heroku-postbuild\": \"next build\",\n\t\t\"start\": \"NODE_ENV=production node server/server.js\"\n\t},\n\t\"now\": {\n\t\t\"name\": \"nextjs-express-mongoose-crudify-boilerplate\",\n\t\t\"alias\": \"nextjs-express-mongoose-crudify-boilerplate\"\n\t},\n\t\"engines\": {\n\t\t\"node\": \"^10.13.0\",\n\t\t\"yarn\": \"^1.3.2\"\n\t},\n\t\"dependencies\": {\n\t\t\"body-parser\": \"^1.15.2\",\n\t\t\"dotenv\": \"^6.2.0\",\n\t\t\"express\": \"^4.14.0\",\n\t\t\"glob\": \"^7.1.2\",\n\t\t\"isomorphic-fetch\": \"^2.2.1\",\n\t\t\"isomorphic-unfetch\": \"^2.0.0\",\n\t\t\"lodash\": \"^4.17.4\",\n\t\t\"mongoose\": \"^4.7.6\",\n\t\t\"mongoose-crudify\": \"^0.2.0\",\n\t\t\"next\": \"^8.0.3\",\n\t\t\"next-redux-wrapper\": \"^3.0.0-alpha.1\",\n\t\t\"next-routes\": \"^1.4.2\",\n\t\t\"react\": \"^16.0.0\",\n\t\t\"react-dom\": \"^16.0.0\",\n\t\t\"react-redux\": \"^5.0.6\",\n\t\t\"redux\": \"^3.7.2\",\n\t\t\"redux-api\": \"^0.11.1\",\n\t\t\"redux-thunk\": \"^2.2.0\"\n\t},\n\t\"devDependencies\": {\n\t\t\"babel-eslint\": \"^10.0.1\",\n\t\t\"jasmine\": \"^3.3.1\",\n\t\t\"nodemon\": \"^1.12.1\",\n\t\t\"standard\": \"^12.0.1\"\n\t}\n}\n"
  },
  {
    "path": "pages/_app.js",
    "content": "// pages/_app.js\nimport React from 'react'\nimport { Provider } from 'react-redux'\nimport App, { Container } from 'next/app'\nimport withRedux from 'next-redux-wrapper'\nimport { makeStore } from '../redux/reduxApi.js'\n\nclass MyApp extends App {\n  static async getInitialProps ({ Component, ctx }) {\n    return {\n      pageProps: {\n        // Call page-level getInitialProps\n        ...(Component.getInitialProps ? await Component.getInitialProps(ctx) : {})\n      }\n    }\n  }\n\n  render () {\n    const { Component, pageProps, store } = this.props\n    return (\n      <Container>\n        <Provider store={store}>\n          <Component {...pageProps} />\n        </Provider>\n      </Container>\n    )\n  }\n}\n\nexport default withRedux(makeStore, { debug: false })(MyApp)\n"
  },
  {
    "path": "pages/index.js",
    "content": "import { Component } from 'react'\n\nimport reduxApi, { withKittens } from '../redux/reduxApi.js'\n\nimport { Link } from '../server/routes.js'\nimport PageHead from '../components/PageHead'\nimport KittenItem from '../components/KittenItem'\n\nclass IndexPage extends Component {\n  static async getInitialProps ({ store, isServer, pathname, query }) {\n    // Get all kittens\n    const kittens = await store.dispatch(reduxApi.actions.kittens.sync())\n    return { kittens, query }\n  }\n\n  constructor (props) {\n    super(props)\n    this.state = { name: '' }\n  }\n\n  handleChangeInputText (event) {\n    this.setState({ name: event.target.value })\n  }\n\n  handleAdd (event) {\n    const { name } = this.state\n    if (!name) return\n    const callbackWhenDone = () => this.setState({ name: '', inProgress: false })\n    this.setState({ inProgress: true })\n    // Actual data request\n    const newKitten = { name }\n    this.props.dispatch(reduxApi.actions.kittens.post({}, { body: JSON.stringify(newKitten) }, callbackWhenDone))\n  }\n\n  handleUpdate (kitten, index, kittenId, event) {\n    const name = window.prompt('New name?', kitten.name)\n    if (!name) return\n    const callbackWhenDone = () => this.setState({ inProgress: false })\n    this.setState({ inProgress: kittenId })\n    // Actual data request\n    const newKitten = { id: kittenId, name }\n    this.props.dispatch(reduxApi.actions.kittens.put({ id: kittenId }, { body: JSON.stringify(newKitten) }, callbackWhenDone))\n  }\n\n  handleDelete (index, kittenId, event) {\n    const callbackWhenDone = () => this.setState({ inProgress: false })\n    this.setState({ inProgress: kittenId })\n    // Actual data request\n    this.props.dispatch(reduxApi.actions.kittens.delete({ id: kittenId }, callbackWhenDone))\n  }\n\n  render () {\n    const { kittens } = this.props// dd\n\n    const kittenList = kittens.data\n      ? kittens.data.map((kitten, index) => <KittenItem\n        key={index}\n        kitten={kitten}\n        index={index}\n        inProgress={this.state.inProgress}\n        handleUpdate={this.handleUpdate.bind(this, kitten)}\n        handleDelete={this.handleDelete.bind(this)}\n      />)\n      : []\n\n    return <main>\n      <PageHead\n        title='Next.js (React) + Express REST API + MongoDB + Mongoose-Crudify boilerplate'\n        description='Demo of nextjs-express-mongoose-crudify-boilerplate'\n      />\n\n      <h1>Kittens</h1>\n\n      {kittenList}\n      <div>\n        <input placeholder='Enter a kitten name' value={this.state.name} onChange={this.handleChangeInputText.bind(this)} disabled={this.state.inProgress} />\n        <button onClick={this.handleAdd.bind(this)} disabled={this.state.inProgress}>Add kitten</button>\n        <style jsx>{`\n          div {\n            margin-top: 1em;\n          }\n        `}</style>\n      </div>\n\n      <h2>Routing</h2>\n      Current page slug: /{this.props.query.slug}\n      <ul>\n        <li><Link route='/about'><a>About</a></Link></li>\n        <li><Link route='/more/contact'><a>Contact</a></Link></li>\n      </ul>\n\n    </main>\n  };\n}\n\nexport default withKittens(IndexPage)\n"
  },
  {
    "path": "redux/reduxApi.js",
    "content": "import _ from 'lodash'\nimport fetch from 'isomorphic-fetch'\n\nimport reduxApi, { transformers } from 'redux-api'\nimport adapterFetch from 'redux-api/lib/adapters/fetch'\nimport { createStore, applyMiddleware, combineReducers } from 'redux'\nimport thunkMiddleware from 'redux-thunk'\nimport { connect } from 'react-redux'\n\nconst { config } = require('../config/config')\n\nconst apiTransformer = function (data, prevData, action) {\n  const actionMethod = _.get(action, 'request.params.method')\n  switch (actionMethod) {\n    case 'POST':\n      return [...prevData, data]\n    case 'PUT':\n      return prevData.map(oldData => oldData._id === data._id ? data : oldData)\n    case 'DELETE':\n      return _(prevData).filter(oldData => oldData._id === data._id ? undefined : oldData).compact().value()\n    default:\n      return transformers.array.call(this, data, prevData, action)\n  }\n}\n\n// redux-api documentation: https://github.com/lexich/redux-api/blob/master/docs/DOCS.md\nconst thisReduxApi = reduxApi({\n\n  // Simple endpoint description\n  // oneKitten: '/api/kittens/:id',\n\n  // Complex endpoint description\n  kittens: {\n    url: '/api/kittens/:id',\n    crud: true, // Make CRUD actions: https://github.com/lexich/redux-api/blob/master/docs/DOCS.md#crud\n\n    // base endpoint options `fetch(url, options)`\n    options: config.jsonOptions,\n\n    // reducer (state, action) {\n    //  console.log('reducer', action);\n    //  return state;\n    // },\n\n    // postfetch: [\n    //  function ({data, actions, dispatch, getState, request}) {\n    //    console.log('postfetch', {data, actions, dispatch, getState, request});\n    //    dispatch(actions.kittens.sync());\n    //  }\n    // ],\n\n    // Reimplement default `transformers.object`\n    // transformer: transformers.array,\n    transformer: apiTransformer\n\n  }\n\n})\n  .use('fetch', adapterFetch(fetch))\n  .use('rootUrl', config.appUrl)\n\nexport default thisReduxApi\n\nconst createStoreWithThunkMiddleware = applyMiddleware(thunkMiddleware)(createStore)\nexport const makeStore = (reduxState, enhancer) => createStoreWithThunkMiddleware(combineReducers(thisReduxApi.reducers), reduxState)\n\n// endpointNames: Use reduxApi endpoint names here\nconst mapStateToProps = (endpointNames, reduxState) => {\n  let props = {}\n  for (let i in endpointNames) {\n    props[endpointNames[i]] = reduxState[endpointNames[i]]\n    props[`${endpointNames[i]}Actions`] = thisReduxApi.actions[endpointNames[i]]\n  }\n  return props\n}\n\nexport const withReduxEndpoints = (PageComponent, endpointNames) => connect(mapStateToProps.bind(undefined, endpointNames))(PageComponent)\n// Define custom endpoints/providers here:\nexport const withKittens = PageComponent => withReduxEndpoints(PageComponent, ['kittens'])\n"
  },
  {
    "path": "server/api/kittens.js",
    "content": "'use strict'\n\nconst mongooseCrudify = require('mongoose-crudify')\n\nconst helpers = require('../services/helpers')\nconst Kitten = require('../models/kitten')\n\nmodule.exports = function (server) {\n  // Docs: https://github.com/ryo718/mongoose-crudify\n  server.use(\n    '/api/kittens',\n    mongooseCrudify({\n      Model: Kitten,\n      selectFields: '-__v', // Hide '__v' property\n      endResponseInAction: false,\n\n      // beforeActions: [],\n      // actions: {}, // list (GET), create (POST), read (GET), update (PUT), delete (DELETE)\n      afterActions: [\n        { middlewares: [helpers.formatResponse] }\n      ]\n    })\n  )\n}\n"
  },
  {
    "path": "server/models/kitten.js",
    "content": "const mongoose = require('mongoose')\n\nconst Schema = mongoose.Schema\n\nconst kittenSchema = new Schema({\n  name: { type: String, required: true }\n})\n\nmodule.exports = mongoose.model('Kitten', kittenSchema)\n"
  },
  {
    "path": "server/routes.js",
    "content": "const routes = require('next-routes')\nconst routesImplementation = routes()\n\n// routesImplementation\n//   .add([identifier], pattern = /identifier, page = identifier)\n//   .add('/blog/:slug', 'blogShow')\n//   .add('showBlogPostRoute', '/blog/:slug', 'blogShow')\n\nroutesImplementation.add('/:slug', 'index')\nroutesImplementation.add('/more/:slug', 'index')\n\nmodule.exports = routesImplementation\n\n// Usage inside Page.getInitialProps (req = { pathname, asPath, query } = { pathname: '/', asPath: '/about', query: { slug: 'about' } })\n"
  },
  {
    "path": "server/server.js",
    "content": "require('dotenv').config()\n\nconst express = require('express')\nconst server = express()\nconst bodyParser = require('body-parser')\nconst mongoose = require('mongoose')\nconst glob = require('glob')\nconst next = require('next')\n\nconst dev = process.env.NODE_ENV !== 'production'\nconst app = next({ dev })\n\nconst routes = require('./routes')\nconst routerHandler = routes.getRequestHandler(app)\n\nconst { config } = require('../config/config')\n\napp.prepare().then(() => {\n  // Parse application/x-www-form-urlencoded\n  server.use(bodyParser.urlencoded({ extended: false }))\n  // Parse application/json\n  server.use(bodyParser.json())\n\n  // Allows for cross origin domain request:\n  server.use(function (req, res, next) {\n    res.header('Access-Control-Allow-Origin', '*')\n    res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')\n    next()\n  })\n\n  // MongoDB\n  mongoose.Promise = Promise\n  mongoose.connect(config.databaseUrl, { useMongoClient: true })\n  const db = mongoose.connection\n  db.on('error', console.error.bind(console, 'connection error:'))\n\n  // REST API routes\n  const rootPath = require('path').join(__dirname, '/..')\n  glob.sync(rootPath + '/server/api/*.js').forEach(controllerPath => {\n    if (!controllerPath.includes('.test.js')) require(controllerPath)(server)\n  })\n\n  // Next.js page routes\n  server.get('*', routerHandler)\n\n  // Start server\n  server.listen(config.serverPort, () => console.log(`${config.appName} running on http://localhost:${config.serverPort}/`))\n})\n"
  },
  {
    "path": "server/services/helpers.js",
    "content": "//\n// Name:    helpers.js\n// Purpose: Library for helper functions\n// Creator: Tom Söderlund\n//\n\n'use strict'\n\nconst _ = require('lodash')\n\n// Since DELETE doesn't return the _id of deleted item by default\nmodule.exports.formatResponse = function (req, res, next) {\n  if (req.crudify.err) console.error('formatResponse:', _.get(req, 'crudify.err.message'))\n  return res.json(req.crudify.err || (req.method === 'DELETE' ? req.params : req.crudify.result))\n}\n"
  },
  {
    "path": "static/app.css",
    "content": "* {\n\tbox-sizing: border-box;\n}\n\nbody {\n\tmargin: 1em;\n\tfont-family: sans-serif;\n\tfont-size: 20px;\n}\n\n@media only screen and (max-width: 480px) {\n\tinput, button {\n\t\twidth: 100%;\n\t}\n}\n\n/* Nice & simple: Button - http://codepen.io/tomsoderlund/pen/qqyzqp */\nbutton {\n\tbackground-color: dodgerblue;\n\tborder-radius: 0.2em;\n\tborder: none;\n\tbox-shadow: 0 0.125em 0.125em rgba(0,0,0, 0.3);\n\tbox-sizing: border-box;\n\tcolor: white;\n\tcursor: pointer;\n\tfont-family: inherit;\n\tfont-size: inherit;\n\tfont-weight: bold;\n\toutline: none;\n\tpadding: 0.6em;\n\tmargin: 0.2em;\n\ttransition: all 0.2s;\n}\nbutton:hover:not(:disabled) {\n\topacity: 0.8;\n\ttransition: box-shadow 0s;\n}\nbutton:active {\n\tmargin-top: 0.3em;\n\tmargin-bottom: 0.1em;\n\tbox-shadow: 0 0.5px 0.125em rgba(0,0,0, 0.4);\n}\nbutton:disabled {\n\tbackground-color: silver;\n}\n\n/* Nice & simple: Input and Dropdown Menu - http://codepen.io/tomsoderlund/pen/GNBbWz */\ninput,\ntextarea,\nselect {\n\toutline: none;\n\tresize: none;\n\tbox-shadow: inset 0 0.125em 0.125em rgba(0,0,0, 0.3);\n\tbox-sizing: border-box;\n\tbackground-color: white;\n\tborder-radius: 0.2em;\n\tborder: 2px solid lightgray;\n\tcolor: inherit;\n\tfont-family: inherit;\n\tfont-size: inherit;\n\tpadding: 0.6em;\n\tmargin: 0.2em;\n}\ninput:hover:not(:disabled),\ntextarea:hover:not(:disabled),\nselect:hover:not(:disabled) {\n\tborder-color: silver;\n}\ninput:focus,\ntextarea:focus,\nselect:focus {\n\tborder-color: darkgray;\n}\ninput:disabled,\ntextarea:disabled,\nselect:disabled {\n\tbackground-color: whitesmoke;\n}"
  }
]