#100DaysOfMERN - Day 17

Subscribe to my newsletter and never miss my upcoming articles

Over the next posts, I'm going to build a mini app that includes a React frontend and a backend server using Express. It'll serve as a sort of template project for later reference, where I document each step while building out the most basic functionality, and extending it bit by bit.

I'll stay away from databases (MongoDB) and use a local file instead, until I'm sufficiently confident with just the very basic stuff as described above.

✏ The "App"

Current functionality:

  • frontend (React) displays a list of books
  • backend (Express) serves the data

This should be sufficiently simple and boring, so it won't distract from the primary goal to learn how to GET-POST-PUT-DELETE.

✏ Setting up the project

Creating a folder named books and initialising a git repository:

mkdir books
cd books
git init

I'll also create a .gitignore file, with the most important entry at this point being /node_modules to make sure I'm not accidentally pushing the whole monster up to github.

Next, setting up the frontend with create-react-app:

npx create-react-app frontend

Creating a backend folder with a package.json:

mkdir backend
cd backend
npm init

✏ The Backend

In the backend folder, I'll first create a subfolder data and put into it a file bookdata.js that exports a list of books:

const books = [{
        "id":"1",
        "title":"You Don't Know JS"
    },
    {
        "id":"2",
        "title":"How to Read a Book"
    },
    {
        "id":"3",
        "title":"The Benefits of Breathing"
    },
    {
        "id":"4",
        "title":"Fake Book Titles: Vol.2"
    },
]
export default books

Setting up the server

I'll use Express to set up the server, as it seems to be the state-of-the-art tool, which is probably reflected by the fact that the E in MERN stands for Express.

npm i express

Next, I need a server.js file in the backend folder. For the most basic setup, it only needs a few lines of code:

server.js

import express from 'express';

const app = express();

app.listen(3001, console.log('server is serving'))

(Note to self: Learn how to set up a server with Vanilla JS to really understand why Express makes my life easier)

Instead of const express = require('express'), I'm using an import statement, so I'll need some adjustments in the package.json. First, setting "type":"module", and also adding a script "start":"node server.js", and setting the entry point to "main":"server.js":

package.json

{
  "name": "backend",
  "type":"module",
  "version": "1.0.0",
  "description": "book app",
  "main": "server.js",
  "scripts": {
    "start":"node server.js"
  },
  "author": "jsdisco",
  "license": "MIT",
  "dependencies": {
    "express": "^4.17.1"
  }
}

If I npm start the project now, I can see that the server is running, as it's logging "server is serving" in the console. However, if I open the page localhost:3001 in the browser, I'm greeted with the message "Cannot GET /" on the page, and a "404 Not Found" error in the browser console.

Setting up the first route

The reason is that I haven't set up any routes yet. I haven't told the server what it's supposed to serve, in case someone makes a GET request to the root directory, indicated by a slash. To help that, I can use Express's app.get(path, callback) function:

import express from 'express';

const app = express();

app.get('/', (request, response) => {
    response.send('<h1>Server is Serving</h1>')
})

app.listen(3001, console.log('server is serving'))

After a server restart, I can now see the <h1> that's coming back in the browser. To avoid having to stop/start the server after every modification, there's this convenient package called nodemon, which I'll install as a dev dependency because it's only needed during development:

Installing nodemon

npm i nodemon --save-dev

Adding another script to the package.json:

  "scripts": {
    "start": "node server.js",
    "dev": "nodemon index.js"
  }

Starting the server with npm run dev will now make nodemon automatically restart the server if it detects any changes in my backend folder.

Checking out the request object

If my server is supposed to serve the book data, it'll have to import it from the file in the data folder. I could now, for example, send back the data to the browser, using the .json() method (it would work with .send(), too, since I'm sending back an array (=object), and the .send() method therefore uses json.stringify on the response).

server.js

import express from 'express';
import books from './data/bookdata.js'

const app = express();

app.get('/', (request, response) => {
    // response.send('<h1>Server is Serving</h1>')
    response.json(books)
})

app.listen(3001, console.log('server is serving'))

I could send back whatever I want, just for testing purposes - or can I? I'm particularly interested in the request parameter of the app.get function. It seems that the server receives a whole LOT of stuff, which I can see if I add a console.log(request) to the app.get callback (note that it'll be logged in the terminal, not the browser console), but sending the request back as response results in an error:

TypeError: Converting circular structure to JSON

The request object is an object that references itself, so it can't be converted to a JSON object and can't be sent. I can, however, send back certain properties of that object, for example:

  • request.headers (I could also see this in the developer tools under the network tab)
  • request.url (self-explanatory)
  • request.method (same)
  • request.params (this will come in handy later, but for now it's just an empty object)
  • request.query (an object with information on whatever I add to the URL as query, like localhost:3001?q=1&p=3)
  • request.route (not sure yet what's the stack property in that object)

Final touches on the server

This all seems like "nice-to-know", but back to the original task. I'll set up another route called "/api/books" to send back the book data:

import express from 'express';
import books from './data/bookdata.js'

const app = express();

app.get('/', (request, response) => {
    response.send('<h1>Server is Serving</h1>')
})

app.get('/api/books', (request, response) => {
    response.json(books)
})

app.listen(3001, console.log('server is serving'))

I'll leave it like this for now and set up a simple frontend.


✏ The Frontend

After cleaning up all files, I'll have only one App.js file to keep stuff at one place for now. All this component does is render the list of books, which it retrieves from an API call to the backend.

First, the skeleton:

App.js

import React, { useState, useEffect } from 'react';

const App = () => {

    const [books, setBooks] = useState([]);

    useEffect(() => {
        const url = 'http://localhost:3001/api/books';
        // getBooks()
    }, [])

    return (
        <div className="app">
            <header><h1>Books</h1></header>
            <div className="book-list"></div>
        </div>
    )
}

export default App

The URL for the GET request depends on how I configured the server routes.

Getting the data with fetch()

One way to write the function getBooks, using only fetch() and .then() syntax:

const getBooks = () => fetch(url)
    .then(res => res.json())
    .then(data => setBooks(data));

Getting the data with axios and .then()

A popular method to make an API request is the axios package, which needs to be installed in the frontend folder:

npm i axios
const getBooks = () => axios.get(url)
    .then(res => setBooks(res.data));

Getting the data with axios and async/await

I haven't covered async/await yet in a blog post, but just for completion:

const getBooks = async () => {
    const res = await axios.get(url);
    setBooks(res.data)
}

I'll stick with the second method for now. The only thing left to do is to actually render the list on the page, so this is my final App component:

App.js

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const App = () => {

    const [books, setBooks] = useState([]);

    useEffect(() => {
        const url = 'http://localhost:3001/api/books';
        const getBooks = () => axios.get(url)
            .then(res => setBooks(res.data));
        getBooks();
    }, [])

    return (
        <div className="app">
            <header><h1>Books</h1></header>
            <div className="book-list">
                {books.map(book => <li key={book.id}>{book.title}</li>)}
            </div>
        </div>
    )
}

export default App

✏ CORS problem

Even though everything is set up correctly, my App doesn't work yet. The reason: My App's frontend is running on localhost:3000, and it attempts to fetch data from a different location (the server is running on localhost:3001). This will result in a CORS error (Cross-Origin Resource Sharing), because the browser won't allow fetching resources from a different origin for security reasons.

To avoid this, I'll have to configure my server so it tells the browser via the HTTP response header that requests from any origin are allowed (wildcard), or that specifically the origin localhost:3000 is allowed. There's a couple of ways to do that:

Setting the "Access-Control-Allow-Origin" response header

Using the Express response.append() method:

server.js

app.get('/api/books', (request, response) => {
    response.append('Access-Control-Allow-Origin', 'http://localhost:3000');
    response.send(books)
})

/* alternatively, allow any requests through a wildcard: */

app.get('/api/books', (request, response) => {
    response.append('Access-Control-Allow-Origin', '*');
    response.send(books)
})

If I have only one route, this isn't too complicated, but for an App with many routes, it wouldn't be very DRY to add that header to each request, so I could also set this once for all routes:

app.use((request, response, next) => {
    response.append('Access-Control-Allow-Origin', '*');
    next();
});

Using the cors package

A shorter way is to import the cors package and use it like so:

npm i cors

server.js

import cors from 'cors';

const app = express();

app.use(cors());

I've tried to put this into a diagram to better understand what's happening:

CORS.png


✏ Recap

I've learned

  • how to set up a project with frontend and backend
  • how to set up a server and server routes with Express
  • how to fetch data from a file with different methods
  • what is a CORS error and how to avoid it

✏ Next:

  • making a POST request to add books to the list

✏ Thanks for reading!

I do my best to thoroughly research the things I learn, but if you find any errors or have additions, please leave a comment below, or @ me on Twitter. If you liked this post, I invite you to subsribe to my newsletter. Until next time 👋


✏ Previous Posts

  • Day 1: Introduction, Node.js, Node.js in the terminal
  • Day 2: npm, node_modules, package.json and package-lock.json, local vs global installation of packages
  • Day 3: Create a React app without create-react-app, Webpack, Babel
  • Day 4: npx and cowsay
  • Day 5: npm vs. npx, npm audit, semantic versioning and update rules
  • Day 6: Call stack, event loop, JavaScript engine, JavaScript runtime
  • Day 7: Call stack and event loop in Node.js, setImmediate()
  • Day 8: setImmediate(), process.nextTick(), event loop phases in Node.js
  • Day 9: Network requests with XMLHttpRequest and callbacks
  • Day 10: Promises
  • Day 11: Network requests with XMLHttpRequest and Promises
  • Day 12: React Quiz App part 1
  • Day 13: React Hangman
  • Day 14: FullStackOpen course 1: details of GET and POST requests, request headers
  • Day 15: React Hangman: Trigger fetch with click event callback vs useEffect
  • Day 16: REST API and CRUD

Comments (2)

Anand Baraik's photo

A good read! 🙂

jsdisco's photo

Thanks, really appreciate the feedback.