├── client ├── src │ ├── index.css │ ├── components │ │ ├── Message.js │ │ ├── Footer.js │ │ ├── Loader.js │ │ ├── Book.js │ │ └── Header.js │ ├── reducers │ │ ├── index.js │ │ └── bookReducers.js │ ├── store.js │ ├── reportWebVitals.js │ ├── index.js │ ├── App.css │ ├── actions │ │ ├── types.js │ │ └── bookActions.js │ ├── App.js │ └── pages │ │ ├── HomePage.js │ │ ├── BookPage.js │ │ └── BookListPage.js ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── nginx │ └── default.conf ├── Dockerfile.dev ├── Dockerfile ├── package.json └── README.md ├── nginx ├── Dockerfile ├── Dockerfile.dev └── default.conf ├── server ├── .env.example ├── utils │ ├── asyncManager.js │ └── libraryError.js ├── Dockerfile ├── Makefile ├── .dockerignore ├── database │ └── db.js ├── routes │ └── bookRoutes.js ├── Dockerfile.dev ├── .eslintrc.json ├── package.json ├── middleware │ └── errorMiddleware.js ├── models │ └── Book.js ├── server.js ├── controllers │ └── bookControllers.js └── yarn.lock ├── README.md ├── LICENSE ├── .gitignore └── docker-compose.yml /client/src/index.css: -------------------------------------------------------------------------------- 1 | main { 2 | min-height: 80vh; 3 | } 4 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.19-alpine 2 | COPY ./default.conf /etc/nginx/conf.d/default.conf -------------------------------------------------------------------------------- /nginx/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM nginx:1.19-alpine 2 | COPY ./default.conf /etc/nginx/conf.d/default.conf -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /server/.env.example: -------------------------------------------------------------------------------- 1 | PORT= 2 | NODE_ENV= 3 | MONGO_ROOT_USERNAME= 4 | MONGO_ROOT_PASSWORD= 5 | DB_NAME= -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/API-Imperfect/mern_library_nginx/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/API-Imperfect/mern_library_nginx/HEAD/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/API-Imperfect/mern_library_nginx/HEAD/client/public/logo512.png -------------------------------------------------------------------------------- /server/utils/asyncManager.js: -------------------------------------------------------------------------------- 1 | module.exports = (fn) => (req, res, next) => { 2 | fn(req, res, next).catch(next); 3 | }; 4 | -------------------------------------------------------------------------------- /client/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 3000; 3 | 4 | location / { 5 | root /usr/share/nginx.html; 6 | index index.html index.htm; 7 | try_files $uri $uri/ /index.html; 8 | } 9 | } -------------------------------------------------------------------------------- /client/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine3.10 2 | 3 | LABEL version="1.0" 4 | LABEL description="React Frontend for the Library API" 5 | 6 | WORKDIR /app 7 | 8 | COPY ./package.json ./ 9 | 10 | RUN npm install 11 | 12 | COPY . . 13 | 14 | CMD [ "npm", "run", "start" ] 15 | 16 | -------------------------------------------------------------------------------- /client/src/components/Message.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Alert } from "react-bootstrap"; 3 | 4 | const Message = ({ variant, children }) => { 5 | return {children}; 6 | }; 7 | 8 | Message.defaultProps = { variant: "info" }; 9 | 10 | export default Message; 11 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine3.10 2 | 3 | LABEL version="1.0" 4 | LABEL description="Production image for the Library MERN API" 5 | 6 | WORKDIR /app 7 | 8 | COPY ["package.json","package-lock.json*","npm-shrinkwrap.json*", "./"] 9 | 10 | RUN npm install 11 | 12 | COPY . . 13 | 14 | CMD [ "npm", "run", "start" ] -------------------------------------------------------------------------------- /server/utils/libraryError.js: -------------------------------------------------------------------------------- 1 | class LibraryError extends Error { 2 | constructor(message, statusCode) { 3 | super(message); 4 | this.statusCode = statusCode; 5 | this.status = `${statusCode}`.startsWith("4") 6 | ? "Client Error" 7 | : "Server Error"; 8 | } 9 | } 10 | 11 | module.exports = LibraryError; 12 | -------------------------------------------------------------------------------- /server/Makefile: -------------------------------------------------------------------------------- 1 | ifneq (,$(wildcard ./.env)) 2 | include .env 3 | export 4 | ENV_FILE_PARAM = --env-file .env 5 | endif 6 | 7 | build: 8 | docker-compose up --build --remove-orphans 9 | 10 | up: 11 | docker-compose up 12 | 13 | down: 14 | docker-compose down 15 | 16 | down-V: 17 | docker-compose down -v 18 | 19 | volume: 20 | docker volume inspect mern-library-nginx_mongodb-data 21 | 22 | -------------------------------------------------------------------------------- /client/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { 3 | bookCreateReducer, 4 | bookDeleteReducer, 5 | bookDetailsReducer, 6 | bookListReducer, 7 | } from "./bookReducers"; 8 | 9 | export default combineReducers({ 10 | bookList: bookListReducer, 11 | bookDetails: bookDetailsReducer, 12 | bookCreate: bookCreateReducer, 13 | bookDelete: bookDeleteReducer, 14 | }); 15 | -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/charts 16 | **/docker-compose* 17 | **/Dockerfile* 18 | **/node_modules 19 | **/npm-debug.log 20 | **/obj 21 | **/secrets.dev.yaml 22 | **/values.dev.yaml 23 | README.md 24 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine3.10 as builder 2 | 3 | LABEL version="1.0" 4 | LABEL description="React Frontend for the Library API" 5 | 6 | WORKDIR /app 7 | 8 | COPY ./package.json ./ 9 | 10 | RUN npm install 11 | 12 | COPY . . 13 | 14 | RUN npm run build 15 | 16 | FROM nginx 17 | EXPOSE 3000 18 | COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf 19 | COPY --from=builder /app/build /usr/share/nginx/html 20 | 21 | -------------------------------------------------------------------------------- /client/src/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from "redux"; 2 | import { composeWithDevTools } from "redux-devtools-extension"; 3 | import reduxThunk from "redux-thunk"; 4 | import rootReducer from "./reducers"; 5 | 6 | const initialState = {}; 7 | 8 | const store = createStore( 9 | rootReducer, 10 | initialState, 11 | composeWithDevTools(applyMiddleware(reduxThunk)) 12 | ); 13 | 14 | export default store; 15 | -------------------------------------------------------------------------------- /client/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /server/database/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const connectToDB = async () => { 4 | const connect = await mongoose.connect(process.env.MONGO_URI, { 5 | dbName: process.env.DB_NAME, 6 | useNewUrlParser: true, 7 | useCreateIndex: true, 8 | useUnifiedTopology: true, 9 | useFindAndModify: false, 10 | }); 11 | console.log(`MongoDB connected: ${connect.connection.host}`); 12 | }; 13 | 14 | module.exports = connectToDB; 15 | -------------------------------------------------------------------------------- /client/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Col, Container, Row } from "react-bootstrap"; 3 | 4 | const Footer = () => { 5 | return ( 6 | 15 | ); 16 | }; 17 | 18 | export default Footer; 19 | -------------------------------------------------------------------------------- /server/routes/bookRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { 3 | createBook, 4 | getAllBooks, 5 | updateBook, 6 | deleteBook, 7 | getBook, 8 | getPublishedBooks, 9 | } = require("../controllers/bookControllers"); 10 | 11 | const router = express.Router(); 12 | 13 | router.route("/books").get(getAllBooks).post(createBook); 14 | router.route("/books/:id").patch(updateBook).delete(deleteBook).get(getBook); 15 | router.route("/books/published").get(getPublishedBooks); 16 | 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /client/src/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Spinner } from "react-bootstrap"; 3 | 4 | const Loader = () => { 5 | return ( 6 | 16 | Loading.... 17 | 18 | ); 19 | }; 20 | 21 | export default Loader; 22 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /server/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # specify base image from dockerhub 2 | FROM node:14-alpine3.10 3 | 4 | LABEL version="1.0" 5 | LABEL description="Development image for the Library MERN API" 6 | 7 | # workdir is where our code shall live in the container 8 | # all commands executed relative to this directory 9 | WORKDIR /app 10 | 11 | COPY ["package.json","package-lock.json*","npm-shrinkwrap.json*", "./"] 12 | 13 | # Install dependencies and clear npm cache 14 | RUN npm install && npm cache clean --force 15 | 16 | COPY . . 17 | 18 | # use EXPOSE command to have our port mapped by the docker daemon 19 | EXPOSE 5000 20 | 21 | # default dev command 22 | CMD [ "npm", "run", "dev" ] -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import App from "./App"; 5 | import "./bootstrap.min.css"; 6 | import "./index.css"; 7 | import reportWebVitals from "./reportWebVitals"; 8 | import store from "./store"; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById("root") 15 | ); 16 | 17 | // If you want to start measuring performance in your app, pass a function 18 | // to log results (for example: reportWebVitals(console.log)) 19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 20 | reportWebVitals(); 21 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const BOOK_LIST_FAIL = "BOOK_LIST_FAIL"; 2 | export const BOOK_LIST_REQUEST = "BOOK_LIST_REQUEST"; 3 | export const BOOK_LIST_SUCCESS = "BOOK_LIST_SUCCESS"; 4 | 5 | export const BOOK_DETAILS_FAIL = "BOOK_DETAILS_FAIL"; 6 | export const BOOK_DETAILS_REQUEST = "BOOK_DETAILS_REQUEST"; 7 | export const BOOK_DETAILS_SUCCESS = "BOOK_DETAILS_SUCCESS"; 8 | 9 | export const BOOK_CREATE_REQUEST = "BOOK_CREATE_REQUEST"; 10 | export const BOOK_CREATE_SUCCESS = "BOOK_CREATE_SUCCESS"; 11 | export const BOOK_CREATE_FAIL = "BOOK_CREATE_FAIL"; 12 | export const BOOK_CREATE_RESET = "BOOK_CREATE_RESET"; 13 | 14 | export const BOOK_DELETE_REQUEST = "BOOK_DELETE_REQUEST"; 15 | export const BOOK_DELETE_SUCCESS = "BOOK_DELETE_SUCCESS"; 16 | export const BOOK_DELETE_FAIL = "BOOK_DELETE_FAIL"; 17 | -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier", "plugin:node/recommended"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "spaced-comment": "off", 7 | "no-console": "warn", 8 | "consistent-return": "off", 9 | "func-names": "off", 10 | "object-shorthand": "off", 11 | "no-process-exit": "off", 12 | "no-param-reassign": "off", 13 | "no-return-await": "off", 14 | "no-underscore-dangle": "off", 15 | "class-methods-use-this": "off", 16 | "prefer-destructuring": ["error", { "object": true, "array": false }], 17 | "no-unused-vars": [ 18 | "error", 19 | { "argsIgnorePattern": "req|res|next|val" } 20 | ], 21 | "arrow-body-style": "off" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /nginx/default.conf: -------------------------------------------------------------------------------- 1 | upstream client { 2 | server client:3000; 3 | } 4 | 5 | upstream library-api { 6 | server library-api:5000; 7 | } 8 | 9 | 10 | server { 11 | listen 80; 12 | 13 | location / { 14 | proxy_pass http://client; 15 | proxy_redirect off; 16 | proxy_set_header Host $host; 17 | proxy_set_header X-Real-IP $remote_addr; 18 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 19 | proxy_set_header X-Forwarded-Host $server_name; 20 | 21 | } 22 | 23 | location /sockjs-node{ 24 | proxy_pass http://client; 25 | proxy_http_version 1.1; 26 | proxy_set_header Upgrade $http_upgrade; 27 | proxy_set_header Connection "Upgrade"; 28 | 29 | } 30 | 31 | location /api/v1 { 32 | proxy_pass http://library-api; 33 | } 34 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Library MERN App 2 | 3 |

4 | 5 | Twitter: AlphaOmondi 6 | 7 |

8 | 9 | 👋 This simple Library MERN Book Application built to demonstrate how The MERN stack can be used with Docker and NGINX 10 | 11 | ## Run the Application 12 | 13 | ``` 14 | git clone https://github.com/API-Imperfect/mern_library_nginx.git 15 | cd mern_library_nginx 16 | cd server 17 | run the command: make build 18 | navigate to localhost:8080 19 | ``` 20 | 21 | If you prefer not using Make files 22 | 23 | ``` 24 | git clone https://github.com/API-Imperfect/mern_library_nginx.git 25 | cd mern_library_nginx 26 | run the command: docker-compose up --build --remove-orphans 27 | navigate to localhost:8080 28 | ``` 29 | 30 | ## License 31 | 32 | MIT 33 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Container } from "react-bootstrap"; 3 | import { BrowserRouter as Router, Route } from "react-router-dom"; 4 | import Footer from "./components/Footer"; 5 | import Header from "./components/Header"; 6 | import BookListPage from "./pages/BookListPage"; 7 | import BookPage from "./pages/BookPage"; 8 | import HomePage from "./pages/HomePage"; 9 | 10 | const App = () => { 11 | return ( 12 | 13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 |
21 |