Build a full-stack application using Node.js, React and Atlas
Tags: atlas, node, react, opensource
Published: 2023-03-21 12:03:07

## Introduction
We will build a full-stack application using Node.js, React and [Atlas](https://atlasgo.io/). 

**[Node.js](https://nodejs.org/en)** 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](https://react.dev/)** is a JavaScript library for building user interfaces. It allows developers to create reusable UI components and manage the state of those components. 

**[Atlas](https://atlasgo.io/)** 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](https://atlasgo.io/)**, which allows for the inspection, modification, schema-changing and migration of databases.

## Prerequisites
Before going further, you need the following:

- [Atlas](https://atlasgo.io/getting-started)
- 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](https://atlasgo.io/):
We’ll design our database schema and migrate it into our database using [atlas](https://atlasgo.io/).

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](https://atlasgo.io/atlas-schema/sql-resources).

```
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](https://atlasgo.io/versioned/diff).


### 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](https://github.com/cseTanvir/todo-node-app-atlas-cli/tree/main/api) 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](https://atlasgo.io/). Thanks for reading so far and I hope you enjoyed it!