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: [![Support Tom on Ko-Fi.com](https://www.tomsoderlund.com/ko-fi_tomsoderlund_50.png)](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/). ![nextjs-express-mongoose-crudify-boilerplate demo on Heroku](docs/kittens-demo.gif) ## 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 }) => (
{kitten.name} Update Delete
) 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}

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; }