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 }) => (
<div className={inProgress === kitten._id ? 'inProgress' : ''}>
{kitten.name}
<a className='update' onClick={handleUpdate.bind(this, index, kitten._id)}>Update</a>
<a className='delete' onClick={handleDelete.bind(this, index, kitten._id)}>Delete</a>
<style jsx>{`
a {
margin-left: 0.5em;
cursor: pointer;
font-size: 0.6em;
text-transform: uppercase;
}
a.update {
color: lime;
}
a.delete {
color: tomato;
}
.inProgress {
opacity: 0.3;
}
`}</style>
</div>
)
export default KittenItem
================================================
FILE: components/PageHead.js
================================================
import Head from 'next/head'
const PageHead = ({ title, description }) => (
<Head>
<title>{title}</title>
<meta name='description' content={description} />
<meta charSet='utf-8' />
<meta httpEquiv='content-language' content='en' />
<meta name='viewport' content='initial-scale=1.0, width=device-width' />
<link rel='stylesheet' href='/static/app.css' />
</Head>
)
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 (
<Container>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</Container>
)
}
}
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) => <KittenItem
key={index}
kitten={kitten}
index={index}
inProgress={this.state.inProgress}
handleUpdate={this.handleUpdate.bind(this, kitten)}
handleDelete={this.handleDelete.bind(this)}
/>)
: []
return <main>
<PageHead
title='Next.js (React) + Express REST API + MongoDB + Mongoose-Crudify boilerplate'
description='Demo of nextjs-express-mongoose-crudify-boilerplate'
/>
<h1>Kittens</h1>
{kittenList}
<div>
<input placeholder='Enter a kitten name' value={this.state.name} onChange={this.handleChangeInputText.bind(this)} disabled={this.state.inProgress} />
<button onClick={this.handleAdd.bind(this)} disabled={this.state.inProgress}>Add kitten</button>
<style jsx>{`
div {
margin-top: 1em;
}
`}</style>
</div>
<h2>Routing</h2>
Current page slug: /{this.props.query.slug}
<ul>
<li><Link route='/about'><a>About</a></Link></li>
<li><Link route='/more/contact'><a>Contact</a></Link></li>
</ul>
</main>
};
}
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;
}
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
SYMBOL INDEX (11 symbols across 2 files)
FILE: pages/_app.js
class MyApp (line 8) | class MyApp extends App {
method getInitialProps (line 9) | static async getInitialProps ({ Component, ctx }) {
method render (line 18) | render () {
FILE: pages/index.js
class IndexPage (line 9) | class IndexPage extends Component {
method getInitialProps (line 10) | static async getInitialProps ({ store, isServer, pathname, query }) {
method constructor (line 16) | constructor (props) {
method handleChangeInputText (line 21) | handleChangeInputText (event) {
method handleAdd (line 25) | handleAdd (event) {
method handleUpdate (line 35) | handleUpdate (kitten, index, kittenId, event) {
method handleDelete (line 45) | handleDelete (index, kittenId, event) {
method render (line 52) | render () {
Condensed preview — 15 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (19K chars).
[
{
"path": ".gitignore",
"chars": 20,
"preview": "node_modules/\n.next/"
},
{
"path": "README.md",
"chars": 3150,
"preview": "# Next.js (React) + Redux + Express REST API + MongoDB + Mongoose-Crudify boilerplate\n\n_Note: this is my v1 boilerplate "
},
{
"path": "components/KittenItem.js",
"chars": 694,
"preview": "const KittenItem = ({ kitten, index, inProgress, handleUpdate, handleDelete }) => (\n <div className={inProgress === kit"
},
{
"path": "components/PageHead.js",
"chars": 417,
"preview": "import Head from 'next/head'\n\nconst PageHead = ({ title, description }) => (\n <Head>\n <title>{title}</title>\n <me"
},
{
"path": "config/config.js",
"chars": 695,
"preview": "const appName = 'nextjs-express-mongoose-crudify-boilerplate'\nconst databaseName = 'nextjs-express-boilerplate'\nconst se"
},
{
"path": "package.json",
"chars": 1392,
"preview": "{\n\t\"name\": \"nextjs-express-mongoose-crudify-boilerplate\",\n\t\"version\": \"4.0.0\",\n\t\"description\": \"Next.js (React) + Redux "
},
{
"path": "pages/_app.js",
"chars": 759,
"preview": "// pages/_app.js\nimport React from 'react'\nimport { Provider } from 'react-redux'\nimport App, { Container } from 'next/a"
},
{
"path": "pages/index.js",
"chars": 3057,
"preview": "import { Component } from 'react'\n\nimport reduxApi, { withKittens } from '../redux/reduxApi.js'\n\nimport { Link } from '."
},
{
"path": "redux/reduxApi.js",
"chars": 2715,
"preview": "import _ from 'lodash'\nimport fetch from 'isomorphic-fetch'\n\nimport reduxApi, { transformers } from 'redux-api'\nimport a"
},
{
"path": "server/api/kittens.js",
"chars": 627,
"preview": "'use strict'\n\nconst mongooseCrudify = require('mongoose-crudify')\n\nconst helpers = require('../services/helpers')\nconst "
},
{
"path": "server/models/kitten.js",
"chars": 205,
"preview": "const mongoose = require('mongoose')\n\nconst Schema = mongoose.Schema\n\nconst kittenSchema = new Schema({\n name: { type: "
},
{
"path": "server/routes.js",
"chars": 533,
"preview": "const routes = require('next-routes')\nconst routesImplementation = routes()\n\n// routesImplementation\n// .add([identifi"
},
{
"path": "server/server.js",
"chars": 1527,
"preview": "require('dotenv').config()\n\nconst express = require('express')\nconst server = express()\nconst bodyParser = require('body"
},
{
"path": "server/services/helpers.js",
"chars": 457,
"preview": "//\n// Name: helpers.js\n// Purpose: Library for helper functions\n// Creator: Tom Söderlund\n//\n\n'use strict'\n\nconst _ ="
},
{
"path": "static/app.css",
"chars": 1479,
"preview": "* {\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 a"
}
]
About this extraction
This page contains the full source code of the tomsoderlund/nextjs-express-mongoose-crudify-boilerplate GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 15 files (17.3 KB), approximately 5.1k tokens, and a symbol index with 11 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.