Whenever I have to create a new project, I prefer to keep my stack with only one language. So I love use javascript for everything, with Node.js, Express.js, React.js and I really like to use NoSQL databases like MongoDB in this case.
So I decided to share my experience of setting up this environment from scratch.
First, let's create a folder and generate our package.json file for this project.
$ mkdir node-react-starter
$ cd node-react-starter
$ npm init -y
Now, let's install the project dependencies
$ npm install --save express body-parser mongoose
In this project we use Express.js, a very popular framework for Node.js applications.
body-parser is used to parse incoming request bodies in a middleware before your handlers, available under the req.body property.
Mongoose is a MongoDB object modeling tool designed to work in an asynchronous environment.
Then, install the development dependencies
$ npm install --save-dev nodemon concurrently
nodemon is a package that runs the node.js application and listen to any file change, updating the entire app.
Concurrently allows us to run multiple npm commands at the same time.
After installing the dependencies, you should get a file like this:
Let's create the project structure
$ mkdir models routes
$ touch index.js
Open the index.js file and add the following code:
// index.js
const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const app = express();
mongoose.Promise = global.Promise;
mongoose.connect(process.env.MONGODB_URI || `mongodb://localhost:27017/node-react-starter`);
app.use(bodyParser.json());
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`app running on port ${PORT}`)
});
After this, you can add a run script inside your package.json file, under scripts:
"server": "nodemon index.js"
At this point, you can run your backend and have a successful connection with the mongodb (MongoDB must be up and running). You can run the script you just created like so:
$ npm run server
Let's initiate our version control to keep track of every change. But first we need to add a .gitignore file in the root of our project with the following content:
node_modules
.idea
Then, we initiate our version control
$ git init
$ git add .
$ git commit -am "first commit"
We successfully created our backend structure, now let's jump to the frontend.
Now, let's create a React app with create-react-app.
$ create-react-app client
Now, in the client directory we have to add our dependencies.
Here we are going to use yarn to add this dependencies.
$ cd client
$ yarn add axios
axios is a very popular promise based HTTP client for the browser and node.js.
For react-scripts >= 0.2.3
For the current react version (and any other react-scripts > 0.2.3), you can simply add the following line to your package.json file in the client directory and that will allow you to proxy your front-end requests to the back-end app.
"proxy": "http://localhost:5000"
For react-scripts < 0.2.3
If you are using an older version of react-scripts you might need to add the following configuration to be able to connect the front-end with the back-end:
$ cd client
$ yarn add http-proxy-middleware
http-proxy-middleware is used to create a proxy from our react app to the backend app while on development.
We can now add the config file to setup the proxy to make requests from our frontend to our backend application.
Remember to add this configuration only if you are using an older react version, being react-scripts < 0.2.3.
In the directory /client/src, add the file setupProxy.js with the following content
// /client/src/setupProxy.js
const proxy = require('http-proxy-middleware')
module.exports = function(app) {
app.use(proxy('/api/*', { target: 'http://localhost:5000' }))
}
In the package.json in the root of the project, let's add the following run scripts:
"client": "npm run start --prefix client",
"server": "nodemon index.js",
"dev": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\"",
"start": "node index.js"
Now your package.json file should look like this:
{
"name": "node-react-starter",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"client": "npm run start --prefix client",
"server": "nodemon index.js",
"dev": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\"",
"start": "node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"mongoose": "^5.6.3"
},
"devDependencies": {
"concurrently": "^4.1.1",
"nodemon": "^1.19.1"
}
}
Now you are able to run the project with the following command:
$ npm run dev
This will run the backend application on port 5000, and the frontend on port 3000.
You should see the react application running on http://localhost:3000
To make our project production ready, we need to add the following lines in our index.js file, right after the app.use(bodyParser.json()) call:
if (process.env.NODE_ENV === 'production') {
app.use(express.static('client/build'));
const path = require('path');
app.get('*', (req,res) => {
res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'))
})
}
This will redirect all the requests to our frontend application, unless we specify any route before this code.
Now let's create a simple interaction to see the proxy connection in action
Add the file Product.js inside the directory /models and insert the following code:
// /models/Product.js
const mongoose = require('mongoose');
const {Schema} = mongoose;
const productSchema = new Schema({
name: String,
description: String,
})
mongoose.model('products', productSchema);
Let's create a route for our backend API.
Add the file productRoutes.js inside the directory /routes and insert the following code:
// /routes/productRoutes.js
const mongoose = require('mongoose');
const Product = mongoose.model('products');
module.exports = (app) => {
app.get(`/api/product`, async (req, res) => {
let products = await Product.find();
return res.status(200).send(products);
});
app.post(`/api/product`, async (req, res) => {
let product = await Product.create(req.body);
return res.status(201).send({
error: false,
product
})
})
app.put(`/api/product/:id`, async (req, res) => {
const {id} = req.params;
let product = await Product.findByIdAndUpdate(id, req.body);
return res.status(202).send({
error: false,
product
})
});
app.delete(`/api/product/:id`, async (req, res) => {
const {id} = req.params;
let product = await Product.findByIdAndDelete(id);
return res.status(202).send({
error: false,
product
})
})
}
We can now import the models and routes files inside our index.js like so:
// /index.js
const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
// IMPORT MODELS
require('./models/Product');
const app = express();
mongoose.Promise = global.Promise;
mongoose.connect(process.env.MONGODB_URI || `mongodb://localhost:27017/node-react-starter`);
app.use(bodyParser.json());
//IMPORT ROUTES
require('./routes/productRoutes')(app);
if (process.env.NODE_ENV === 'production') {
app.use(express.static('client/build'));
const path = require('path');
app.get('*', (req,res) => {
res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'))
})
}
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`app running on port ${PORT}`)
});
Now if we run the project we are able to make requests to our simple product api using the url http://localhost:5000/api/product.
Here we can get, insert, update and delete a product.
Back to the react application, lets add a service to make requests to the backend application.
Inside the folder /client/src create a folder called services and add a file productService.js with the following content:
// /client/src/services/productService.js
import axios from 'axios';
export default {
getAll: async () => {
let res = await axios.get(`/api/product`);
return res.data || [];
}
}
Now let's edit the App.js file, adding a simple UI that shows a list of products:
// /client/src/App.js
import React, { useState, useEffect } from "react";
// SERVICES
import productService from './services/productService';
function App() {
const [products, setproducts] = useState(null);
useEffect(() => {
if(!products) {
getProducts();
}
})
const getProducts = async () => {
let res = await productService.getAll();
console.log(res);
setproducts(res);
}
const renderProduct = product => {
return (
<li key={product._id} className="list__item product">
<h3 className="product__name">{product.name}</h3>
<p className="product__description">{product.description}</p>
</li>
);
};
return (
<div className="App">
<ul className="list">
{(products && products.length > 0) ? (
products.map(product => renderProduct(product))
) : (
<p>No products found</p>
)}
</ul>
</div>
);
}
export default App;
At this point, you can run the application again using the command npm run dev, and you will see the following screen:
Use a HTTP client like Postman or Insomnia to add some products. Make a POST request to http://localhost:5000/api/product with the following JSON content:
{
"name": "<product name>",
"description": "<product description here>"
}
Now, you will be able to see a list of products rendered on the screen, like so:
I hope you may find this tutorial useful and in the following days I will continue this tutorial showing how to Dockerize this app.
Also check this next post explaining how to deploy this app to heroku.
If you are interested in working with containers, I also made this post that explains How to dockerize this app and deploy to Heroku.
The source code can be found here
Oldest comments (73)
The proxy isn't working for me
Hello Rajat,
Can you tell me if testing the backend and frontend separately works correctly?
I just gave it another try here and everything works perfect.
Another option you could try is to config the proxy directly in the package.json, the latest versions of react supports this. It is as simple as adding the following line to your package.json file:
"proxy": "localhost:5000"
Adding proxy key worked. Thanks! 😃
Great Rajat!! :D
Hi Thiago! Great article.
I have one question. Once I send a post request for new products, and I look at the app, I just see bullets, but no product names or descriptions. Any idea why that might be?
Thanks!
Hey Tomrains, thank you! :)
If you can see the bullets in your view after creating products, this should probably be a problem in your view.
I recommend you put a console.log(product) inside the renderProduct arrow function, right in the beginning.
Let me know what you got there.
Hey Thiago, here's what I see in the console after I added in console.log(product) to the renderProduct arrow function:
thepracticaldev.s3.amazonaws.com/i...
Humm I see, actually the problem must be when you are creating the products.
What response do you get after making the post request to create a product?
You should get something like this:
{
"error": false,
"product": {
"id": "5dab7eb6fc634a193f1f05f6",
"name": "product 1",
"description": "description",
"_v": 0
}
}
Here's what I'm getting:
thepracticaldev.s3.amazonaws.com/i...
So for some reason the response doesn't have the info I put in?
Oh I see tomrains, you are sending a post request with a x-www-form-urlencoded, when you should actually be sending a json.
I think in postman you should select the option raw.
Give it a try and let me know if it worked! :)
It woooorked! Thanks so much Thiago. I'm having fun building my MERN app now :D
Greaaaat!! Very glad to help :D
Hi Thiago,
I have a similar problem to the one reported by @tomrains , but the answer you gave did not allow me to solve it.
(Note: I'm new to Postman)
the response I get for localhost:5000/api/product?name=to... is
{"error":false,"product":{"id":"5ee204bbf1af38409c11d4de","_v":0}}
the response code is 201 Created.
In the Pretty - Raw - Preview section, I can't edit, it's read only
I added a Content-type: raw section in the headers but that didn't change anything.
I tried using curl, with this command: curl --location --request POST "localhost:5000/api/product" --header "Content-Type: raw" -d '{"name":"toto","description":"tata"}'
but the result seems to be the same...
Here's a screenshot of my postman app:
dev-to-uploads.s3.amazonaws.com/i/...
Hi Francis.
In your screenshot, I can see that you are sending query params with the values when you should actually be sending body data in postman.
You can select the body option and you will have a raw option below it.
Follow a screenshot below with an example:
dev-to-uploads.s3.amazonaws.com/i/...
thank you so much, Thiago! That was really quick! It works now. It seems obvious once you know where to look.
This is a great article!
And a big thank you, for also showing the configuration for production/deployment.
I feel that it's often left out, and for a beginner in the web development field, this is key!
Keep up the good work!
Thank you, Jonas!
Let me know if you need any help :)
When I try to run the app, I get an error like the one below :/ which is annoying as it's a dependency. Anyone know what to do about that?
TypeError: Cannot use 'in' operator to search for 'autoCreate' in true
at NativeConnection.Connection.openUri (/Users/vhu02/Documents/Apprentice_work/node-react-starter/node_modules/mongoose/lib/connection.js:561:22)
at Mongoose.connect (/Users/vhu02/Documents/Apprentice_work/node-react-starter/node_modules/mongoose/lib/index.js:332:15)
at Object. (/Users/vhu02/Documents/Apprentice_work/node-react-starter/index.js:11:10)
at Module._compile (module.js:653:30)
at Object.Module._extensions..js (module.js:664:10)
at Module.load (module.js:566:32)
at tryModuleLoad (module.js:506:12)
at Function.Module._load (module.js:498:3)
at Function.Module.runMain (module.js:694:10)
at startup (bootstrap_node.js:204:16)
at bootstrap_node.js:625:3
[nodemon] app crashed - waiting for file changes before starting...
Hey Veronika,
Could you share the code that reproduces this error, please?
There is nothing much to share as I just followed these steps haha
OMG really? haha
I just download the source code and tested it here. Everything works fine.
Could you try to clone the repo and test it just in case?
yeah so I forked your Github, and the proxy still isn't working. I seem to get a deprecation error
What you could try to do is to add the following line in your client/package.json file:
"proxy": "localhost:5000"
That should solve the problem.
I did try that as the other comment addressed it and it didn't seem to work.
the exact error I'm getting is this:
DeprecationWarning: current URL string parser is deprecated, and will be removed in a future version. To use the new parser, pass option { useNewUrlParser: true } to MongoClient.connect.
events.js:183
throw er; // Unhandled 'error' event
Veronika,
Does the error persist once you modify the line:
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/node-react-starter',{ useNewUrlParser: true });
as suggested by the Deprecation Warning?
What is the prompt error if so?
This deprecation warning should not prevent you from running the app, but the rpacheco suggestion should do the trick.
You can also add this config on another line, like so:
mongoose.set("useNewUrlParser", true);
You might also find other deprecation warnings in the future, follow a list below in case you need it.
mongoose.set("useFindAndModify", false);
mongoose.set("useCreateIndex", true);
mongoose.set("useUnifiedTopology", true);
Let me know if that worked for you.
For the proxy issue follow the steps below.
Replace the ' const proxy' in setupProxy.js file with this.
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use('/api',
createProxyMiddleware({
target: 'localhost:5000',
changeOrigin: true
})
);
};
it works. You can follow . Please let me know here
Small correction - productRoutes.js referenced as productRoute.js (singular) in this page.
Oh, true! Thank you very much for reporting that, I will fix it. :)
Thanks Thiago, nice tutorial. I notice your code doesn't have 'require('dotenv').config()' or the npm package install in the instructions??
Am I missing something, are you using some native way of reading the .env file, thanks!
Alan
Hi Alan, thank you.
I am not using a .env file here, I am only using the process.env variables, assuming that the additional custom variables would be defined on the server.
Thanks Thiago,
I think I see what happened, I'm connecting to a db on Mongo Atlas and copied a line of code from another project!! Thanks for looking, cheers.
Alan
Great guide, thank you!
Question, any idea why the proxy breaks when using a api url more than two levels deep? "/api/blocks" works whereas "/api/blocks/all" does not. Everything else is the same and api routes work separately.
Tried "/api" and "/api/**" in setupProxy.js to no avail.
Hi Alg, sorry for the delayed answer and thank you for your comment.
This deep routes should work as the other ones. What I recommend to you is to use the proxy config that react supports now.
You can simply put the following line in your package.json file:
"proxy": "localhost:5000"
This should do the trick.
Let me know if that works for you!
hi,
nice article and i almost followed the steps, but in my sample application I just want notification from node express so for that I used socket.io and socket.io-client so in test application if I set proxy in setupProxy like app.use(proxy('/', { target: 'localhost:5000' })) than its not working, but if i added "proxy": "localhost:5000" in package.json than its working.
i want to set proxy in setupProxy file.
Thanks
Hi there.
I recommend you to stick with the package.json proxy approach, now that it is supported in the latest version (I don't know exactly since when lol).
I will probably update that in the post as soon as I can.
What to do if we want to deploy multiple apps? They must share the same database, correct?
Hi Jack,
That depends on the project you are working on.
Currently, with almost everything migrating to microservices approach, it is common to have one database for each service/functionality.
[0] [nodemon] app crashed - waiting for file changes before starting...
[1] proxy is not a function
It seems like you do not have the http-proxy-middleware package installed.