#100DaysOfMERN - Day 30

#100DaysOfMERN - Day 30

·

14 min read

✏ #100DaysOfMERN - The App

After learning how to use Mongoose yesterday, I actually couldn't wait to start with my app, so I filled the database with data for the first ~25 days, set up a server, and built a simple frontend to display some post "cards" including tags for each post. Here's what it looks like (don't judge me on the styles, I'm not a designer 🎨) - if you hover over a tag (like create-react-app), it highlights every post with that tag:

app1.jpg

While it was great fun to build, I realised that once the server and the database connection are working, which they already are, I would mostly be busy with frontend stuff again. If you have only 100 records max in your database, and want to perform operations like filtering/sorting, you'd normally fetch that data once, and do the filtering on the frontend. So I'm going to pretend that I'm dealing with huge amounts of data here, and implement some filter functions that re-fetch only specific posts. Just for fun.

But first - documenting the steps that brought me to this point. I've a project folder mern, and within it, a backend folder, that I npm init-ialised to have my package.json.


✏ App - Backend

For the server, I'll use express again. I've written about how to set up a server when I built that mini book app (Day 17 and following), so I'll only include the code with some comments here:

The Server

server.js

const express = require('express');
const cors = require('cors');

// starting the server
const app = express();

// avoid pesky CORS errors
app.use(cors());

// process json data
app.use(express.json());

// process form data
app.use(express.urlencoded({ extended: true }))

// first route to test
app.get('/', (req, res) => {
    res.send('server is serving')
})

const PORT = 3001;
app.listen(PORT, () => console.log(`server running on port ${PORT}`))

The Database

The Post Model

Within a models folder, I've a postmodel.js file, where I use mongoose to define a Schema and export the model:

postmodel.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

// create schema
const PostSchema = new Schema({
    title: {type:String, required:true},
    tags:[{type:String, required:true}],
    summary:[{type:String}],
    published:{type:String, required:true},
    slug:{type:String, required:true}
}, { timestamps: true })

// create model
const PostModel = mongoose.model('post', PostSchema);

module.exports = PostModel

DB connection

Creating a connect folder with a connection.js file, where I bring in the Post Model and establish a connection (I'd like to keep the connection stuff separate from the server, so I'm exporting the mongoose.connection object):

connection.js

const mongoose = require('mongoose');
const PostModel = require('../models/postmodel.js')

const connStr = 'mongodb://localhost/merndb';

const connOptions = {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  serverSelectionTimeoutMS: 5000
};


mongoose.connect(connStr, connOptions)
    .then(() => console.log('connected'))
    .catch(err => console.log(`connection error: ${err}`))

const conn = mongoose.connection;
module.exports = conn;

Serving Data from the Database

If the server is supposed to send the data to the client, it'll need the connection object and also the Post Model. I'll set up the first route to /api/posts, which will send back the whole collection:

server.js

const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');
const PostModel = require('./models/postmodel.js');
const conn = require('./connect/connection.js');

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }))
app.use(cors());

app.get('/', (req, res) => {
    res.send('server is serving')
})

// route to get the whole collection
app.get('/api/posts', (req, res) => {

    PostModel.find()
            .then(data => res.json(data))
            .catch(err => console.log(err))
})

const PORT = 3001;
app.listen(PORT, () => console.log(`server running on port ${PORT}`))

✏ Connecting the Frontend

So far, the frontend doesn't even exist yet, so I'll move back into my mern project folder and initialise it with create-react-app:

npx create-react-app frontend

After cleaning up the files, my App component looks like this:

App.js

import React from 'react';

function App() {

    return (
        <div className="app">APP</div>
    )
}

export default App;

Not much going on there yet... Now, when the component mounts, I'd like to fetch the data from the /api/posts endpoint. I'm using axios for this, so after installing it as a frontend dependency, I'll include a useEffect with an empty dependency array to get the data, and store it in state:

import React, { useState, useEffect } from 'react';
import Posts from './components/Posts.js';
import axios from 'axios';


function App() {

    const [posts, setPosts] = useState(null);

    useEffect(() => {
        const uri = 'http://localhost:3001/api/posts';
        axios.get(uri).then(res => setPosts(res.data));
    },[])

    return (
        <div className="app">
            {posts && <Posts posts={posts} />}
        </div>
    )
}

export default App;

Here, Posts is a simple component, that maps over the posts array and renders the title and the tags. For the "hover" effect as demonstrated in the image at the beginning of this blog, each tag has a onMouseEnter and an onMouseLeave event attached to it, which either stores the currently hovered tag in state, or resets it to an empty string.


✏ Adding a Filter Function

What I'd like to do now is add a filter: when I click on a tag, I'd like to show only posts with that tag. As mentioned above, usually you'd do that on the frontend, but I'd learn nothing new - so I'll instead fetch the data again, but modify the query.

To trigger the re-fetch, I'll add another state variable to my <App/> component, and pass down an updater function to the <Posts/> component:

const [tagFilter, setTagFilter] = useState('')

function updateTagFilter(str){
    setTagFilter(str)
}

Now whenever this variable changes, I'm going to make a new GET request, and add a query string to the URL:

useEffect(() => {
        const uri = 'http://localhost:3001/api/posts';
        const query = `?tag=${tagFilter}`;

        axios.get(uri+query).then(res => setPosts(res.data));

    }, [tagFilter])

Handling the Filter Route on the Server

The endpoint stays the same, I'm still making a request to /api/posts, but now I'll first check the request object to see if there's a query object with a tag property. If there is one, I'll search the collection for posts that have this specific tag in their tags array, by passing an object to the .find method. The operator $in works pretty much like Array.protoype.includes(), so the syntax is relatively intuitive:

app.get('/api/posts', (req, res) => {

    if (req.query.tag){
        PostModel.find({ tags: {$in: req.query.tag}})
            .then(data => res.json(data))
            .catch(err => console.log(err))
    } else {
        PostModel.find()
            .then(data => res.json(data))
            .catch(err => console.log(err))
    }
})

And that's all it takes - simple 😃

Note: The $in operator also works with arrays, so I could also search for posts that include multiple tags:

PostModel.find({ tags: {$in: [tag1, tag2, tag3]}})

For the sake of the user experience, it makes sense now to add a button to reset the tagFilter to an empty string (= "show all" button), but this is pure frontend/React.


While building all this, which wasn't exactly complicated, it transpired that the most work in this App would be filling the database with detailed information about each post. The queries themselves aren't terribly difficult, and I'm only making GET requests. I actually doubt that I'll learn much with this project alone.

I'll keep playing with it though (because that long list of links at the end of each post still annoys me), add some features, fill the database, migrate it to MongoDB Atlas (cloud), and figure out how to deploy my Mini-Meta-App with Heroku. So there'll be some more posts about this.

But in order to learn more about handling databases (including POST, PUT and DELETE requests), I think I need a new project. One that comes to mind (and that I've already built once with a SQL database and PHP) is a recipe database. I like cooking, and while some people just open their cookbook when they look for a recipe, I think it's so much more awesome to open a browser and start a local server instead. So - that's next.


Footnote

I think I've written multiple times in the last posts that whenever you want to work with MongoDB, it has to be running in the background, because that's what I've heard in every tutorial I've followed. I've been working on my app all morning and totally forgot about this, but everything worked perfectly fine. So I'm kind of confused (you know these moments when you wonder "Why on earth does my code even work?"). I'll come back to this once I figure it out.


✏ Recap

I've learned

  • how to set up a backend with server and database
  • how to connect the frontend and either fetch all or only specific records

✏ Next:

  • building a recipe database

✏ 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
  • Day 17: Boring Book App part 1: React Frontend, Express Backend, GET requests, CORS
  • Day 18: Boring Book App part 2: POST request, File System API
  • Day 19: Boring Book App part 3: Request Parameters, DELETE request
  • Day 20: Boring Book App part 4: PUT request
  • Day 21: Express JS vs Vanilla JS part 1: Server setup, routes
  • Day 22: Express JS vs Vanilla JS part 2: Serve static files with Vanilla Server
  • Day 23: Express JS vs Vanilla JS part 3: Serve static files with Express Server, Middleware
  • Day 24: Express JS: express.Router, Postman
  • Day 25: Express JS: express-handlebars
  • Day 26: MongoDB: Installation, noSQL database structure, Mongo Shell commands
  • Day 27: MongoDB: Project setup, JSON vs BSON, connecting to database, inserting a record
  • Day 28: MongoDB: read and modify data with CRUD operations
  • Day 29: MongoDB with Mongoose: Connecting to the database, Models, Schemas, Saving to the database