DEV Community

Cover image for Build a full-stack application using Node.js, React and Atlas
Md Tanvir Hossain
Md Tanvir Hossain

Posted on

Build a full-stack application using Node.js, React and Atlas

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

Enter fullscreen mode Exit fullscreen mode

Our first step will be to create the project folder:

mkdir api
mkdir front

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

To run MySQL:

docker run --rm -d --name atlas-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=pass -e MYSQL_DATABASE=todo_atlas mysql


Enter fullscreen mode Exit fullscreen mode

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 fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

If you open /api/schema/schema.hcl with editor, you’ll find the schema:

schema "todo_atlas" {
  charset = "utf8mb4"
  collate = "utf8mb4_0900_ai_ci"
}

Enter fullscreen mode Exit fullscreen mode

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]
  }
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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 ..

Enter fullscreen mode Exit fullscreen mode

Let's start a new project in /api folder:

npm init -y

Enter fullscreen mode Exit fullscreen mode

Now we need to install these dependencies:

npm install express cors dotenv nodemon

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
    }
}

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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 });
    }
  }


Enter fullscreen mode Exit fullscreen mode

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' })
    }
  }

Enter fullscreen mode Exit fullscreen mode

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' })
    }
}

Enter fullscreen mode Exit fullscreen mode

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 })
    }   
}

Enter fullscreen mode Exit fullscreen mode

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 })
        }      
    }

Enter fullscreen mode Exit fullscreen mode

Finally, We’ll export our controller functions so that it is accessible from the router:

module.exports = {
    createTodo,
    getAllTodos,
    updateTodo,
    deleteTodo,
    deleteAll
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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}`)
})

Enter fullscreen mode Exit fullscreen mode

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"
  },
Enter fullscreen mode Exit fullscreen mode

Run Api

npm start 
or 
npm run dev

Enter fullscreen mode Exit fullscreen mode

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 ./ 

Enter fullscreen mode Exit fullscreen mode

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"
  }
}

Enter fullscreen mode Exit fullscreen mode

Now we need to install all the dependencies:

npm install

Enter fullscreen mode Exit fullscreen mode

Create tailwind.config.js:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}",],
  theme: {
    extend: {},
  },
  plugins: [],
}

Enter fullscreen mode Exit fullscreen mode

Create postcss.config.js :

module.exports = {
    plugins: [
      require('tailwindcss'),
      require('autoprefixer'),
    ]
  }

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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 />);

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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; 

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

Run Front-end

npm start
Enter fullscreen mode Exit fullscreen mode

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)