Step-by-Step Guide to Creating a Fullstack Mood Tracker CRUD App with React, Node.js, and SQLite.
Introduction
Hi all, welcome, and thanks for stopping by!
In this step-by-step guide, we will learn how to create a full-stack application with create, read, update, and delete (CRUD) functionality.
We will use Node.js and SQLite for the backend, and React for the frontend to create our user interface (UI).
Our project will be a mood tracker app where users can input their mood on a scale of 1-10. The app will display these moods in a list, with options to edit or delete them. Additionally, we will include a linear chart to track and visualize mood trends.
The final version will look like this:
Note: This tutorial assumes you have Node.js installed and an IDE installed preferably VScode. If not please go here to install Node.js and here for VScode.
Ok without further ado, let’s jump to it!
Backend Development
We will start with the backend. First, create a backend directory and open it in your favorite IDE. For this article, we will use VS Code.
After opening the backend directory in your code editor, open the terminal and run the following command:
npm init -y
This will register our project as a Node application and create a package.json file inside our directory.
Next, we need to add our project dependencies.
We will use Express to create our server, SQLite3 for our database, and Body-Parser to extract the body of requests.
In the terminal, run:
npm install express sqlite3 body-parser
After the installation is complete, we will set up our directory structure.
The folder structure inside the backend directory should look like this (create directories and files as needed):
/node_modules
/db
mood.db
/routes
index.js
mood.js
/models
mood.js
server.js
package.json
Open server.js, the main file of the Node.js application, and copy and paste the following code:
const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
let db = new sqlite3.Database('./db.sqlite');
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
We imported this code's express, sqlite3, and body-parser modules and initialized a new Express application.
We used body-parser as middleware to parse incoming request bodies. We also initialized a new SQLite database (or opened it if it already exists).
Defined a route handler for GET requests to the root URL (/), and started the server on port 3000.
To run the app with hot reload (so you don't have to restart the server each time you make a change). We can use a dependency called nodemon.
Install it by running:
npm install --save-dev nodemon
To use nodemon, update the scripts section inside package.json. Change the start script to look like this:
"start": "nodemon server.js"
Open your terminal and run:
npm start
Then, open your default browser and navigate to http://localhost:3000/. You should see "Hello World".
Your terminal should display the following message:
[nodemon] starting `node server.js`
Server started on port 3000
Connected to the mood database.
Congrats, you have created and run an Express server!
If you don’t see it running, check your terminal for any errors.
Now let's start working on the model and routes for our application.
A model defines the structure of an object, and routes perform the CRUD operations.
Open models/mood.js and add the following code:
const sqlite3 = require('sqlite3').verbose();
let db = new sqlite3.Database('./db/moods.db', (err) => {
if (err) {
console.error(err.message);
}
console.log('Connected to the moods database.');
});
db.run(`CREATE TABLE IF NOT EXISTS moods(
id INTEGER PRIMARY KEY AUTOINCREMENT,
mood INTEGER NOT NULL
)`, (err) => {
if (err) {
console.error(err.message);
}
console.log("Moods table created");
});
module.exports = db;
In this code, we imported SQLite and defined our database. Created our table with id and mood columns, and exported our database.
Next, let's create our routes. Copy and paste the following code:
const express = require("express");
const router = express.Router();
const db = require("../models/mood");
router.post("/", (req, res) => {
const { mood } = req.body;
const query = `INSERT INTO moods(mood) VALUES(?)`;
db.run(query, [mood], function (err) {
if (err) {
console.error(err.message);
return res.status(500).json({ message: err.message });
}
res.send({ id: this.lastID, mood: mood });
});
});
router.get("/", (req, res) => {
const query = `SELECT * FROM moods`;
db.all(query, [], (err, rows) => {
if (err) {
console.error(err.message);
return res.status(500).json({ message: err.message });
}
res.send(rows);
});
});
router.put("/:id", (req, res) => {
const { mood } = req.body;
const { id } = req.params;
const query = `UPDATE moods SET mood = ? WHERE id = ?`;
db.run(query, [mood, id], function (err) {
if (err) {
console.error(err.message);
return res.status(500).json({ message: err.message });
}
res.send({ id: id, mood: mood });
});
});
router.delete("/:id", (req, res) => {
const { id } = req.params;
const query = `DELETE FROM moods WHERE id = ?`;
db.run(query, [id], function (err) {
if (err) {
console.error(err.message);
return res.status(500).json({ message: err.message });
}
res.send({ changes: this.changes });
});
});
module.exports = router;
Here, we imported Express to create a server application.
Using Express, we made a router object to define routes for the mood application.
We also imported our recently created database model.
For our CRUD operations, we created a new route for each action:
Create: This route responds to all POST requests at the root path (/). We extract the mood from the request and then define a SQL query to insert a new mood. Run the query on our db module, passing the mood value as a parameter. We handle errors and send the response back to the client or a status code of 500 with the error message.
Read: This route responds only to GET requests at the root path (/). We define a SQL query to select all moods, run the query, and send back the response with moods if successful. Otherwise, we send a status 500 code with the error message.
Update: This route responds to PUT requests at the path /:id, mood id should be in the path like /6. Like POST, we extract the body from the request and get the id from the path. We create a query to update a specific mood based on the id. Then run the query, passing mood and id as parameters, and include error handling logic.
Delete: This route responds only to DELETE requests at the path /:id. We get the id from the request, then create a query to delete it from the moods table if it exists, and apply the same error handling logic.
We exported our router object to use the routes in our server.js file. Let's update our server.js file to include our router.
Also, remove the database file reference we moved inside our model. Updated server.js:
const express = require("express");
const bodyParser = require("body-parser");
const moodsRouter = require("./routes/mood");
const cors = require("cors");
const app = express();
app.use(cors());
app.use(bodyParser.json());
app.use("/moods", moodsRouter);
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(3001, () => {
console.log("Server started on port 3001");
});
Notice we also added the CORS middleware for local development and switched the port to 3001.
This will be helpful when integrating and testing our UI. In your terminal, run the following command to install CORS:
npm install --save-dev cors
Find out more about CORS here.
Now start your server app, and let’s test the routes. We can use curl inside the terminal to test.
Here's how you can do it for each type of request:
- GET request: To fetch all moods, use the following command:
curl http://localhost:3001/moods
- POST request: To create a new mood, use the -d flag to send data:
$body = @{
mood = 7
} | ConvertTo-Json
$response = Invoke-WebRequest -Uri http://localhost:3001/moods -Method POST -Body $body -ContentType "application/json"
- PUT request: To update a mood, you need to know the id of the mood you want to update. Replace :id with the actual id of the mood:
# Replace :id with the actual id of the mood you want to update
$id = ":id"
$body = @{
mood = 8
} | ConvertTo-Json
$response = Invoke-WebRequest -Uri http://localhost:3001/moods/$id -Method PUT -Body $body -ContentType "application/json"
- DELETE request: To delete a mood, you also need to know the id of the mood. Replace :id with the actual id of the mood:
# Replace :id with the actual id of the mood you want to delete
$id = ":id"
$response = Invoke-WebRequest -Uri http://localhost:3001/moods/$id -Method DELETE
Check your console for any errors.
You made it all the way here, congratulations! You built an application with CRUD functionality!
Now we are ready to move to the UI.
Frontend Development
For UI development, we first need to create a React app.
We'll use a tool called Create React App, which provides us with a template application to build on.
Open a new terminal, change the directory to frontEnd, and then type the following command:
npx create-react-app mood-tracker-ui
npx allows us to run npm packages without installing them.
It runs create-react-app, the third argument is the directory's name and application.
Now that we've created our app, change the directory to mood-tracker-ui:
cd mood-tracker-ui
Let's clean up by removing files and code we won't use.
Remove the following files: App.test.js, logo.svg, reportWebVitals.js, and setupTests.js.
We'll clean up index.js to look like this:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Next, App.js should look like this: this:
function App() {
return (
<div className="App">
<h1>Hello World</h1>
</div>
);
}
export default App;
Now let's set up our folder structure and run our application. The folder structure should look like this (create directories and files as needed):
/frontend
/src
/components
/MoodInput
/MoodList
/MoodChart
/services
/apiService.js
/assets
/styles
/main.css
App.js
index.js
/public
index.html
package.json
Inside your terminal, run the following command:
npm run start
A new tab will open in your default browser and navigate to http://localhost:3000/. You should see something like this:
Great! Let's start with our API service to add our CRUD functionality. We need methods for GET, POST, PUT, and DELETE requests.
Open apiService.js and insert the following code:
const API_URL = "http://localhost:3001";
export const getMoodsAPI = async () => {
const response = await fetch(`${API_URL}/moods`);
const data = await response.json();
return data;
};
export const addMoodAPI = async (mood) => {
const response = await fetch(`${API_URL}/moods`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ mood }),
});
const data = await response.json();
return data;
};
// Update mood
export const updateMoodAPI = async (id, updatedMood) => {
const response = await fetch(`${API_URL}/moods/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updatedMood),
});
if (!response.ok) {
throw new Error("Failed to update mood");
}
return response.json();
};
// Delete mood
export const deleteMoodAPI = async (id) => {
const response = await fetch(`${API_URL}/moods/${id}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error("Failed to delete mood");
}
return response.json();
};
Here's what each function does:
- getMoodsAPI: Fetches all moods from the API.
- addMoodAPI: Sends a POST request to add a new mood.
- updateMoodAPI: Sends a PUT request to update a specific mood by ID.
- deleteMoodAPI: Sends a DELETE request to remove a mood by ID.
Now that our API service is ready, let's build our components.
Start with MoodInput.js. Open the file and paste this code:
import { useState } from "react";
const MoodInput = ({ addMood }) => {
const [mood, setMood] = useState("");
const [error, setError] = useState(null);
const handleSubmit = (e) => {
e.preventDefault();
if (mood === "" || mood > 10) {
setError("Please enter a value between 1 and 10");
} else {
setError(null);
addMood(mood);
setMood("");
}
};
return (
<form onSubmit={handleSubmit} className="mood-form">
<label htmlFor="mood-input">
On a scale of 1-10, how are you feeling today?
</label>
<input
id="mood-input"
type="number"
min="1"
max="10"
value={mood}
onChange={(e) => setMood(e.target.value)}
/>
<button type="submit">Submit</button>
{error && <p>{error}</p>}
</form>
);
};
export default MoodInput;
In this component:
We use the useState hook to manage the mood and error states. The handleSubmit function handles form submission. Validates the mood input, and adds the mood if valid.
The form includes an input for the mood and a submit button. If the input has an error we render an error message. Finally, we export the MoodInput component.
Next, let's create the MoodList component. This component will display all moods. Paste the following code inside the MoodList.js file:
import { useState } from "react";
import {
updateMoodAPI,
deleteMoodAPI,
} from "../../services/apiService/moodService";
const MoodList = ({ moods, deleteMood, updateMood }) => {
const [editingMoodId, setEditingMoodId] = useState(null);
const [editingMoodValue, setEditingMoodValue] = useState("");
const handleUpdateMood = async (id) => {
try {
const updated = await updateMoodAPI(id, { mood: editingMoodValue });
updateMood(id, updated);
setEditingMoodId(null);
setEditingMoodValue("");
} catch (error) {
console.error(error);
}
};
const handleDeleteMood = async (id) => {
try {
await deleteMoodAPI(id);
deleteMood(id);
} catch (error) {
console.error(error);
}
};
return (
<div className="mood-list">
<ul>
{moods?.map((mood, index) => (
<li key={index}>
{index}-
{editingMoodId === mood.id ? (
<>
<input
type="number"
min="1"
max="10"
value={editingMoodValue}
onChange={(e) => setEditingMoodValue(e.target.value)}
/>
<button onClick={() => handleUpdateMood(mood.id)}>Save</button>
</>
) : (
<>
{mood.mood}
<button
onClick={() => {
setEditingMoodId(mood.id);
setEditingMoodValue(mood.mood);
}}
>
Edit
</button>
</>
)}
<button onClick={() => handleDeleteMood(mood.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
};
export default MoodList;
In this component:
- We import the necessary hooks and API services.
- We define the MoodList functional component, initializing the state for the ID and value of the edited mood.
- The handleUpdateMood and handleDeleteMood methods use the API service methods for updating and deleting moods. We use try-catch blocks for error handling.
- When rendering, we map over the moods and create a list item for each one. If the current mood is being edited, an input and save button are displayed. Otherwise, we display the current mood with an edit button. The delete button is always available. *Finally, we export the MoodList component.
We'll add a chart component for a visual representation of the moods.
This component uses the open-source libraries react-chartjs-2 and chart.js. Run the following command to install them:
npm install --save react-chartjs-2 chart.js
After installing the dependencies, paste the following code inside the MoodChart component:
import React from "react";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from "chart.js";
import { Line } from "react-chartjs-2";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
const MoodChart = ({ moods }) => {
const moodValues = moods?.map((mood) => Number(mood.mood));
const data = {
labels: moods?.map((mood, index) => `Day ${index + 1}`),
datasets: [
{
label: "Mood",
data: moodValues,
borderColor: "rgb(75, 192, 192)",
backgroundColor: "rgba(75, 192, 192, 0.5)",
},
],
};
const options = {
responsive: true,
plugins: {
legend: {
position: "top",
},
title: {
display: true,
text: "Mood Chart",
},
},
scales: {
x: {
type: "category",
},
y: {
type: "linear",
beginAtZero: true,
},
},
};
return <Line options={options} data={data} />;
};
export default MoodChart;
In this component:
- We import the necessary libraries and components.
- Then register the required Chart.js components.
- Inside our MoodChart component. we map through the moods array to extract mood values and labels.
- We define the data and options for the chart.
- The Line chart component is rendered with the required properties.
- Finally, we export the MoodChart component.
Now that we have our components, it's time to import them into App.js. Add the following code:
import { useState, useEffect } from "react";
import MoodChart from "./components/MoodChart/MoodChart";
import MoodInput from "./components/MoodInput/MoodInput";
import MoodList from "./components/MoodList/MoodList";
import { getMoodsAPI, addMoodAPI } from "./services/apiService/moodService";
import "./App.css";
function App() {
const [moods, setMoods] = useState([]);
const updateMood = (id, updatedMood) => {
setMoods((prevMoods) =>
prevMoods.map((mood) => (mood.id === id ? updatedMood : mood))
);
};
const deleteMood = (id) => {
setMoods((prevMoods) => prevMoods.filter((mood) => mood.id !== id));
};
useEffect(() => {
const fetchMoods = async () => {
const moods = await getMoodsAPI();
setMoods(moods);
};
fetchMoods();
}, []);
const addMood = async (mood) => {
const newMood = await addMoodAPI(mood);
setMoods([...moods, newMood]);
};
return (
<div className="app">
<div className="mood-header">
<h1>Mood Tracker</h1>
<MoodInput addMood={addMood} />
</div>
<div className="mood-container">
<MoodList
moods={moods}
deleteMood={deleteMood}
updateMood={updateMood}
/>
<MoodChart moods={moods} />
</div>
</div>
);
}
export default App;
In this file:
- We import the necessary libraries and newly created components.
- Inside our App component, we initialize the state for moods. Define methods for handling mood creation, updates, and deletions.
- The useEffect hook fetches all moods from our database when the component renders.
- We render our components and pass them the necessary props.
- Finally, we export the App component.
Run the app and you should see the full functionality now, make sure your server app is running too, and check your console for any errors.
Amazing! You’ve reached this point, we’ve successfully created a full-stack application. Now, we need to add some styling. In the next section, we’ll cover minor styling tweaks, but feel free to get creative and add your flair.
Styling
Open App.css and paste the following code:
body {
background: mintcream;
}
.mood-header {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
.mood-container {
display: grid;
grid-template-columns: 1fr 1fr 1fr 0.5fr 0.5fr;
gap: 10px;
}
.mood-form > * {
margin-left: 10px;
}
.mood-list {
grid-column: 2;
}
.mood-list li {
margin-bottom: 10px;
}
.mood-list li button {
margin-left: 10px;
}
.mood-chart {
grid-column: 3 / span 2;
}
Here’s what we’re doing:
- Body: Setting the background color to mintcream.
- Header: Using Flexbox to center items vertically and horizontally, with some space below.
- Mood Container: Using a grid layout to position MoodList and MoodChart. MoodList takes one column, and MoodChart spans two columns, both centered.
- Mood List: Adding space between list items and buttons.
Import App.css in your App.js file, then reload your application to see the changes.
Conclusion
Throughout this journey, we built a full-stack application with a React UI, an Express.js server, and SQLite for data storage. We also added some styling using Flexbox and Grid layouts.
Next Steps
Here are some ideas for further improvements:
- Testing: Add tests using Jest and React Testing Library.
- Functionality: Enhance features, like adding real dates to moods.
- Styling: Add more styles, such as icons instead of buttons, custom fonts, and shadows.
- Deployment: Deploy the application.
Thanks for sticking with it! You’ve learned a thing or two. Please share your feedback and like the article.
Subscribe to my newsletter
Read articles from Jesus Esquer directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by