Introduction
We will build a full-stack application using Node.js, React and Atlas.
Node.js is an open-source, cross-platform, back-end JavaScript runtime environment that runs on the V8 engine and executes JavaScript code outside a web browser. It is designed to build scalable network applications and is often used for building server-side applications. Node.js provides an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.
React is a JavaScript library for building user interfaces. It allows developers to create reusable UI components and manage the state of those components.
Atlas is an open-source database schema management tool. This tool allows you to inspect and modify your database, change schemas, and migrate your data. With the Atlas, , designing and creating new schemas for your database is straightforward, without the complexities of SQL syntax.
By the end of the article, you will have a full-stack base that can be extended easily. Additionally, you will be introduced to Atlas, which allows for the inspection, modification, schema-changing and migration of databases.
Prerequisites
Before going further, you need the following:
- Atlas
- Docker
- MySQL database
- Node
- Npm
- JavaScript
- VS-Code (You can use any IDE or editor)
You are also expected to have basic knowledge of these technologies.
Getting Started
Project structure:
project/
api/
config/
dbConfig.js
controllers/
TodoController.js
Models/
index.js
todoModel.js
Routes/
todoRoutes.js
schema/
schema.hcl (encoding must be UTF-8)
.env
index.js
package.json
front/
public/
index.html
src/
App.js
index.css
NewTodo.js
Todo.js
TodoList.js
package.json
postcss.config.js
tailwind.config.js
Our first step will be to create the project folder:
mkdir api
mkdir front
Database inspect, design and migration using Atlas:
We’ll design our database schema and migrate it into our database using atlas.
For macOS + Linux:
curl -sSf https://atlasgo.sh | sh
For windows:
Download atlas from the latest release, rename it to atlas.exe, and move it to
"C:\Windows\System32" and access "atlas" from the PowerShell anywhere.
To run MySQL:
docker run --rm -d --name atlas-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=pass -e MYSQL_DATABASE=todo_atlas mysql
This command creates a new container with the name “atlas-mysql” and sets the root user’s password to “pass“. It also creates a new database called “todo_atlas“.
Connect to docker terminal:
docker exec -it atlas-mysq bash
Enter this command to docker terminal to access mysql:-u flag mean the user, ie ”root” is the user and -p flag mean the password, ie. “pass” is the password.
mysql -uroot -ppass
Create a new user and add privilege:
CREATE USER 'todo_atlas_user'@'%' IDENTIFIED BY 'todo_atlas_password';
GRANT ALL ON todo_atlas.* TO 'todo_atlas_user'@'%';
FLUSH PRIVILEGES;
This will create a new user with the username “todo_atlas_use” and grant all privilege on the database name “todo_atlas”.
Now create a schema folder inside /api
:
cd api
mkdir schema
cd schema
Type the following to inspect your database through the atlas command:
atlas schema inspect -u "mysql://todo_atlas_user:todo_atlas_password@localhost:3306/todo_atlas" > schema.hcl
If you open /api/schema/schema.hcl
with editor, you’ll find the schema:
schema "todo_atlas" {
charset = "utf8mb4"
collate = "utf8mb4_0900_ai_ci"
}
Now let’s design our database for our application (make sure schema.hcl has UTF-8 encoding):
We will create one table name “todos” which will have four columns: "id", "title", "description" and "completed".
To define a table we’ll use "table" keyword.
To define a column we’ll use "column" keyword.
To define primary index we’ll use "primary_key" keyword.
To define index we’ll use "index" keyword.
We’ll define our schema using DDL(Data definition Language). You can learn more about DDL at Atlas-DDL.
schema "todo_atlas" {
charset = "utf8mb4"
collate = "utf8mb4_0900_ai_ci"
}
table "todos" {
schema = schema.todo_atlas
column "id" {
null = false
type = bigint
unsigned = true
auto_increment = true
}
column "title" {
null = false
type = varchar(41)
}
column "description" {
null = true
type = text
}
column "completed" {
null = false
type = bool
default = 0
}
primary_key {
columns = [column.id]
}
index "todos_UN" {
unique = true
columns = [column.title]
}
}
It’s Time to migrate our schema into our database using “declarative schema migration” by following this simple command:
atlas schema apply -u "mysql://todo_atlas_user:todo_atlas_password@localhost:3306/todo_atlas" --to file://schema.hcl
Our schema is successfully migrated and we are ready for creating our todo-app.
There is another type of migration which is versioned schema migration. To know more visit versioned workflow.
API Setup
Now that the database is all set up and all the dependencies have been installed, it’s time to create our application backend.
Get back to our /api
folder from /api/schema
by typing:
cd ..
Let's start a new project in /api
folder:
npm init -y
Now we need to install these dependencies:
npm install express cors dotenv nodemon
Create Database Connector
We first need a connection to the database so that we can use in our application.
First, let's install the following dependencies:
npm install sequelize
Let's create a .env
with required variables in /api
folder:
#database
HOST ='localhost'
USER ='todo_atlas_user'
PASSWORD ='todo_atlas_password'
DATABASE ='todo_atlas'
#application backend
PORT = 5000
Create dbConfig.js
in /api/configs/
:
const dotenv = require('dotenv');
let result = dotenv.config();
module.exports = {
HOST: process.env.HOST,
USER: process.env.USER,
PASSWORD: process.env.PASSWORD,
DB: process.env.DATABASE,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
}
Create Model
We’ll create the model to interact with our database. We’ll create our model todo in the todoModel.js
:
module.exports = (sequelize, DataTypes) => {
const todo = sequelize.define("todos", {
title: {
type: DataTypes.STRING
},
description: {
type: DataTypes.TEXT
},
completed: {
type: DataTypes.BOOLEAN
}
}, {
timestamps: false // disable timestamps
})
return todo
}
We’ll access our model from index.js
, Create index.js
in /api/models/
const dbConfig = require('../configs/dbConfig.js');
const {Sequelize, DataTypes} = require('sequelize');
const sequelize = new Sequelize(
dbConfig.DB,
dbConfig.USER,
dbConfig.PASSWORD, {
host: dbConfig.HOST,
dialect: dbConfig.dialect,
operatorsAliases: false,
pool: {
max: dbConfig.pool.max,
min: dbConfig.pool.min,
acquire: dbConfig.pool.acquire,
idle: dbConfig.pool.idle
}
}
)
sequelize.authenticate()
.then(() => {
console.log('connected...')
})
.catch(err => {
console.log('Error :'+ err)
})
const db = {}
db.Sequelize = Sequelize
db.sequelize = sequelize
db.todo = require('./todoModel.js')(sequelize, DataTypes)
db.sequelize.sync({ force: false })
.then(() => {
console.log('yes re-sync done!')
}).catch(err => {
console.log('Error :'+ err)
})
module.exports = db
Create Controller
Create todoController.js
in /api/controllers
, we’ll create a controller function for every route-request:
First, we’ll import and initialise our model:
const db = require('../models')
const todos = db.todo
We’ll create a controller function createTodo
, for creating new todo:
// 1. create todo
const createTodo = async (req, res) => {
let info = {
title: req.body.title,
description: req.body.description ? req.body.description : "No description yet" ,
published: 0
}
try {
// Check if the title already exists in the database
let todo = await todos.findOne({ where :{title : info.title }});
if (todo!=null) {
// Title already exists, return a 409 (Conflict) error
res.status(409).json({ message: 'Title already exists' });
return;
}
// Title does not exist, insert the new todo into the database
if(req.body.title.length<41){
let todo = await todos.create(info);
res.status(201).json(todo);
}
else{
res.status(409).json({ message: 'Title is too long' });
}
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Internal Server Error'+err });
}
}
We’ll create another function getAllTodos
, for getting all our todo list:
// 2. get all todos
const getAllTodos =async (req, res) => {
try {
let todo = await todos.findAll()
console.log(todo)
res.status(200).json(todo)
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Internal Server Error' })
}
}
We’ll create updateTodo
, to update the todo status when we need to change:
// 3. update todo by id
const updateTodo = async (req, res) => {
try{
let id = req.params.id
let todo = await todos.update(req.body, { where: { id: id }})
res.status(200).send(todo)
}catch(err){
console.error(err);
res.status(500).json({ message: 'Internal Server Error' })
}
}
We’ll create another function deleteTodo
, for deleting a todo by its Id:
// 4. delete todo by id
const deleteTodo = async (req, res) => {
try{
let id = req.params.id
await todos.destroy({ where: { id: id }} )
res.status(200).send('Todo is deleted !')
}catch(err){
console.error(err);
res.status(500).json({ message: 'Internal Server Error'+err })
}
}
We’ll create last controller function deleteAll
, for deleting all todo in the list:
// 5. delete all todos
const deleteAll = async (req, res) => {
try{
await todos.destroy({truncate : true} )
res.status(200).send('All todos are deleted !')
}catch(err){
console.error(err);
res.status(500).json({ message: 'Internal Server Error'+err })
}
}
Finally, We’ll export our controller functions so that it is accessible from the router:
module.exports = {
createTodo,
getAllTodos,
updateTodo,
deleteTodo,
deleteAll
}
Create Router
We'll define all routes regarding our API.
A router defines a set of routes, each associated with a specific HTTP method (e.g., GET, POST, PUT, DELETE). When a request is made to a route, the router will match the URL to the appropriate route and execute the associated code in Controller.
Create todoRoutes.js
in /api/routes
, we’ll create a router for every route-request:
// import controllers
const todoController = require('../controllers/todoController')
// router instance
const router = require('express').Router()
// defining routes
router.get('/todos', todoController.getAllTodos)
router.post('/todos',todoController.createTodo)
router.put('/todos/:id',todoController.updateTodo)
router.delete('/todos/:id',todoController.deleteTodo)
router.delete('/delete-all',todoController.deleteAll)
module.exports = router
Create Api
This is our API. This application manages all the requests and processes through the controller and middleware.
Create server.js
in /api
folder:
const express = require('express')
const cors = require('cors')
// expess initializes
const app = express()
// middleware
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// router
const router = require('./routes/todoRoutes.js')
app.use('/api/v1', router)
//port
const PORT = process.env.PORT || 5000
//server
app.listen(PORT, () => {
console.log(`server is running on port ${PORT}`)
})
Now configure package.json
for running our api:
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon server",
"start": "node server"
},
Run Api
npm start
or
npm run dev
The difference between two are, npm start
don’t support hot reloading but npm run dev
does.
Our api should be running on port 5000.
If you are confused about the dependencies, visit the GitHub directory.
Frontend Setup
Initialize React
Goto /front
and create React app by the following command:
npx create-react-app ./
Copy the following in /front/package.json
:
{
"name": "Todo-Frontend",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:5000",
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"autoprefixer": "^10.4.14",
"axios": "^1.3.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"tailwindcss": "^3.2.7"
}
}
Now we need to install all the dependencies:
npm install
Create tailwind.config.js
:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}",],
theme: {
extend: {},
},
plugins: [],
}
Create postcss.config.js
:
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
]
}
Delete all content in /front/src
and Create index.js
in /front/src/
:
Create index.css
in /front/src
:
@tailwind base;
@tailwind components;
@tailwind utilities;
Create index.css
in /front/src
:
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(<App />);
Create components
We’ve installed all the dependencies for React and also configured TailwindCSS. It’s time to make the components for the Frontend of the application.
Create NewTodo.js
in /front/src
:
import React, { useState } from 'react';
const NewTodo = ({ addTodo }) => {
const [title, setTitle] = useState('');
const handleSubmit = e => {
e.preventDefault();
addTodo({
title: title,
completed: false
});
setTitle('');
};
return (
<form onSubmit={handleSubmit}>
<input type="text" placeholder="Add a new Todo" value={title} onChange={e => setTitle(e.target.value)} />
<button type="submit">Add</button>
</form>
);
};
export default NewTodo;
Create Todo.js
in /front/src
:
import React from 'react';
const Todo = ({ todo, deleteTodo, toggleCompleted }) => {
return (
<div className="todo">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleCompleted(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>{todo.title}</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</div>
);
};
export default Todo;
Create TodoList.js
in /front/src
:
import React from 'react';
import Todo from './Todo';
const TodoList = ({ todos, deleteTodo, toggleCompleted }) => {
return (
<div className="todo-list">
{todos.map(todo => (
<Todo key={todo.id} todo={todo} deleteTodo={deleteTodo} toggleCompleted={toggleCompleted} />
))}
</div>
);
};
export default TodoList;
Create Front-end
We already made our components, now we’ll create the frotend of our Todo app.
Create App.js
in /front/src
:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const base = "api/v1";
function App() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
const [error, setError] = useState('');
useEffect(() => {
axios.get(base+'/todos')
.then(response => {
setTodos(response.data);
})
.catch(error => {
console.log(error);
});
}, []);
const handleInputChange = (event) => {
setNewTodo(event.target.value);
};
const handleAddTodo = () => {
if (newTodo.trim() === '') {
return;
}
axios.post(base+'/todos', { title: newTodo })
.then(response => {
setTodos([...todos, response.data]);
setNewTodo('');
setError('');
})
.catch(error => {
setError(error);
console.log(error);
});
};
const handleDeleteTodo = (id) => {
axios.delete(base+`/todos/${id}`)
.then(response => {
setTodos(todos.filter(todo => todo.id !== id));
})
.catch(error => {
console.log(error);
});
};
const handleToggleTodo = (id) => {
const updatedTodos = todos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed;
}
return todo;
});
axios.put(base+`/todos/${id}`, { completed: updatedTodos.find(todo => todo.id === id).completed })
.then(response => {
setTodos(updatedTodos);
})
.catch(error => {
console.log(error);
});
};
return (
<div className='container mx-auto bg-mnblue'>
<div className="flex flex-col items-center h-screen bg-grey-300">
<h1 className=' py-2 font-bold text-white'>Todo App</h1>
<div className='flex-col py-2 mb-2' >
<input aria-label="Todo input" className="mr-2 shadow appearance-none border rounded w-80 py-2 px-3 text-black leading-tight focus:outline-none focus:shadow-outline "type="text" value={newTodo} onChange={handleInputChange} placeholder="Add task." />
<button className="shadow bg-mint px-3 hover:bg-mint-light focus:shadow-outline focus:outline-none text- font-bold py-1 px-1 rounded" onClick={handleAddTodo} >Add Todo</button>
{error? (<div className='mt-2 p-1 text-center bg-gray-300' style={{ color: 'red' }}>{error.response.data.message}</div>) : (<div></div>)}
</div>
<div style={{borderTop:"solid 2px black"}}></div>
<ul className="flex flex-col w-full " style={{ listStyle: 'none' , maxWidth: '500px'}} >
{todos.map(todo => (
<li className='bg-saffron flex p-1 m-1 rounded' key={todo.id} >
<input aria-label="Todo status toggle" className="px-2 " type="checkbox" checked={todo.completed} onChange={() => handleToggleTodo(todo.id)} />
<span className="mx-2 text-center flex-1 " style={{ textDecoration: todo.completed ? 'line-through' : 'none', color: todo.completed ? '#FB4D3D' : 'black' }}>{todo.title}</span>
<button className="float-end bg-tomato hover:bg-white hover:text-tomato focus:shadow-outline focus:outline-none text-white font-bold mx-auto mr-1 px-1 rounded" onClick={() => handleDeleteTodo(todo.id)} >Delete</button>
</li>
))}
</ul>
</div>
</div>
);
}
export default App;
Run Front-end
npm start
Conclusion
In this tutorial we learned how to create a full-stack application with Node.js, React and Atlas. Thanks for reading so far and I hope you enjoyed it!
Top comments (0)