DEV Community

Cover image for A day in the life of a developer - Building a dashboard app with SQL, Node.js, Django and Next.js
Andrew Baisden
Andrew Baisden

Posted on

A day in the life of a developer - Building a dashboard app with SQL, Node.js, Django and Next.js

Introduction

Today we will be getting a quick insight into what it's like to work on projects as a developer as part of a team inside of a company. We are going to be building a dashboard application which has two backends. One will be created using Node.js and the other using Django. The frontend will be created using Next.js and Tailwind. The dashboard is used for adding animes to a database. You can perform full CRUD actions, and filter by release year and the data will be displayed in a chart.

https://res.cloudinary.com/d74fh3kw/image/upload/v1706037484/dashboard-app_vjvhv8.gif

This project will be divided into these 3 sections:

  1. Jira project workflow
  2. Building the backend
  3. Building the frontend

You can find the full codebase on GitHub here Anime Dashboard App.

With our introduction out of the way let's move on to the Jira project workflow.

Jira project workflow

The Product Owner and the Development Team, are often facilitated by the Scrum Master when setting up a project. This stage can be different for each company and this is just one example of how it could be done.

  1. Creating a Jira project
  2. Create epics, stories, bugs, and tasks in the backlog section and then decide which ones to add to the next sprint
  3. Use confluence for documentation and project management
  4. Start the sprint

A developer could have several tools installed which could include:

  • A GitHub repo for the project
  • Slack or alternative
  • Jira or alternative
  • IDE or Code editor and development environment setup for work
  • API testing tool for the backend (Postman, Insomnia, REST Client, Swagger etc...)

Now let's see a quick example of what a developer's workflow could look like in a project in the next section.

Developer workflow

This example could apply to any developer who is working on the team. They would need to:

  1. Pick up a Jira ticket from the current sprint or backlog and then assign the ticket to yourself and work on it by changing its status from to do to in progress
  2. Create a new development branch for the ticket with a naming convention that matches the ticket so it is easy to find for example feature-123, bug-123, task-123 etc...
  3. Work on the story and when you are done push your changes to the repo so that it goes through the CI/CD workflow by creating a pull request (PR) and changing the status to β€œin review”
  4. Add the PR to the Jira story as a web link or similar
  5. Share the story you worked on in the team chat app like Slack with the appropriate message for another developer to review your changes

For example "PR for https://project/browse/feature-123 is ready for review"

Another developer will check the PR, approve it and merge it if all the tests pass and there are no errors.

Now repeat the process by returning to step 1 and picking up another ticket to work on.

With that brief explanation out of the way let's move on to the prerequisites for this project.

Prerequisites

  • PostgreSQL installed
  • MySQL installed
  • SQLite installed
  • Database Management System installed (Azure Data Studio or alternative)
  • Node and npm installed
  • Python installed with virtualenv or an alternative

Now with our prerequisites done let's begin!

Building the backend

Let's just assume that a backend developer has picked up a ticket from Jira for setting up the backend architecture. The first stage would be to set up the SQL databases.

Setup a SQL database

Our application will only need one database however I have included steps for creating a database for PostgreSQL, MySQL and SQLite so you can use whichever one you want.

PostgreSQL and MySQL database setup

Connect to the PostgreSQL and MySQL databases using Azure Data Studio or the database management tool of your choice. This is how my setup looks in Azure Data Studio if you use Visual Studio Code you will get used to the design easily because they are both made by Microsoft.

https://res.cloudinary.com/d74fh3kw/image/upload/v1705963411/azure-data-studio_xjjmzb.jpg

This is how you would create a database using Azure Data Studio. Please make sure that you change the database from Postgres or MySQL after you run the SQL query below. We want to create our tables in the manga database, not the default.

Now create a SQL database by running the query below in your database application of choice:

CREATE DATABASE manga;
Enter fullscreen mode Exit fullscreen mode

https://res.cloudinary.com/d74fh3kw/image/upload/v1706094204/dashboard-app_h4492b.jpg

Next, select the database you just made and create a table that has a primary key. The SQL for primary keys differs for both PostgreSQL and MySQL however the SQL for all other properties remains the same.

These SQL queries essentially create two tables called anime and anime_details inside of our manga database. The column and data types are self-explanatory as you can see below. Both tables have a column called anime_id which we will use later for joining both tables together using a SQL join clause.

Copy, paste and run the SQL queries into their corresponding database:

PostgreSQL table setup
CREATE TABLE anime (
id SERIAL PRIMARY KEY,
anime_id VARCHAR(10),
anime_name VARCHAR(100),
anime_release VARCHAR(10)
);

CREATE TABLE anime_details (
id SERIAL PRIMARY KEY,
anime_id VARCHAR(10),
anime_genre VARCHAR(50),
anime_rating INT
);
Enter fullscreen mode Exit fullscreen mode
MySQL table setup
CREATE TABLE anime (
id INT AUTO_INCREMENT PRIMARY KEY,
anime_id VARCHAR(10),
anime_name VARCHAR(100),
anime_release VARCHAR(10)
);

CREATE TABLE anime_details (
id INT AUTO_INCREMENT PRIMARY KEY,
anime_id VARCHAR(10),
anime_genre VARCHAR(50),
anime_rating INT
);
Enter fullscreen mode Exit fullscreen mode

In the next section, we will repeat the same process but this time for an SQLite database. Before we begin create a folder on your computer for the dashboard project called anime-dashboard-app.

SQLite database setup

Azure Data Studio does not currently support SQLite databases. So you can either use the command line to manage your database or find a GUI application that supports SQLite. This is how the SQLite database looks when using the command line:

https://res.cloudinary.com/d74fh3kw/image/upload/v1706032164/sqllite-database_kpfzzl.jpg

Use your terminal application to cd into the anime-dashboard-app folder and then use the command line to create an SQLite database using the code below:

sqlite3 manga.db
Enter fullscreen mode Exit fullscreen mode

We now need to create some tables for our SQLite database. They are going to be the same as the ones we created for our PostgreSQL and MySQL databases however the SQL query will be different. Copy and paste this code into your terminal to run the query:

CREATE TABLE anime (
id INTEGER PRIMARY KEY AUTOINCREMENT,
anime_id VARCHAR(10),
anime_name VARCHAR(100),
anime_release VARCHAR(10)
);

CREATE TABLE anime_details (
id INTEGER PRIMARY KEY AUTOINCREMENT,
anime_id VARCHAR(10),
anime_genre VARCHAR(50),
anime_rating INT
);
Enter fullscreen mode Exit fullscreen mode

With our database and tables set, the next stage will be to add some data to our databases which we are going to do in the next section.

Creating SQL statements for our SQL databases

It is now time for us to create the SQL queries which we will be using to give us full CRUD (Create, Read, Update, Delete) functionality in our databases. These SQL queries will work with all of our SQL databases.

Let's start with the SQL query for creating data.

CREATE

The SQL query below inserts data into both tables. All you have to do is run the query in the databases we created like before:

INSERT INTO anime (anime_id, anime_name, anime_release)
VALUES ('1234567890', 'Naruto', '1999'), ('0987654321', 'Bleach', '2004');

INSERT INTO anime_details (anime_id, anime_genre, anime_rating)
VALUES ('1234567890', 'Action', 10), ('0987654321', 'Action', 10);
Enter fullscreen mode Exit fullscreen mode

Now that we have data in our databases let's check to see if we can access them with the read statement queries up next.

READ

If you run these SQL queries in our databases you should be able to see the data that we added in the tables:

SELECT * FROM anime;
Enter fullscreen mode Exit fullscreen mode
SELECT * FROM anime_details;
Enter fullscreen mode Exit fullscreen mode

And here is how we will read the data using a condition like the ID:

SELECT * FROM anime WHERE id = '1';
Enter fullscreen mode Exit fullscreen mode
SELECT * FROM anime_details WHERE id = '1';
Enter fullscreen mode Exit fullscreen mode

With this code below, we can do an inner join and combine both tables on the anime_id key. This SQL query selects unique keys for the table so there is no duplication:

SELECT anime.anime_id, anime_name, anime_genre, anime_release anime_rating FROM anime
INNER JOIN anime_details
ON anime.anime_id = anime_details.anime_id;
Enter fullscreen mode Exit fullscreen mode

If we wanted everything including duplicates we could use the wildcard:

SELECT * FROM anime
INNER JOIN anime_details
ON anime.anime_id = anime_details.anime_id;
Enter fullscreen mode Exit fullscreen mode

Up next will be our updated SQL queries.

UPDATE

To update data in the tables we could use these SQL queries in our database:

UPDATE anime
SET anime_name = 'Dragonball Z', anime_release = '1989'
WHERE anime_id = '1234567890';

UPDATE anime_details
SET anime_genre = 'Adventure', anime_rating = 9
WHERE anime_id = '1234567890';
Enter fullscreen mode Exit fullscreen mode

Now we will learn how to do updates using inner joins.

Performing an INNER JOIN

Inner joins allow us to combine two tables by referencing records that have matching values in the same tables.

This is how we would do an INNER JOIN in PostgreSQL:

UPDATE anime
SET anime_name = 'Attack on Titan'
FROM anime_details
WHERE anime.anime_id = anime_details.anime_id
AND anime.anime_id = '0987654321';

UPDATE anime_details
SET anime_genre = 'Fantasy'
WHERE anime_id = '0987654321';
Enter fullscreen mode Exit fullscreen mode

And this is how we would do an INNER JOIN in MySQL:

UPDATE anime
INNER JOIN anime_details
ON anime.anime_id = anime_details.anime_id
SET anime.anime_name = 'Attack on Titan', anime_details.anime_genre = 'Fantasy'
WHERE anime.anime_id = '0987654321';
Enter fullscreen mode Exit fullscreen mode

SQLite does not support the use of INNER JOIN directly in the UPDATE syntax so this is how we would achieve the desired result by using a subquery instead:

UPDATE anime
SET anime_name = 'Attack on Titan'
WHERE anime_id = '0987654321';

UPDATE anime_details
SET anime_genre = 'Fantasy'
WHERE anime_id = '0987654321';
Enter fullscreen mode Exit fullscreen mode

Lastly, let's see how we can delete data from our databases.

DELETE

To delete data from a table use this type of SQL query:

DELETE FROM anime
WHERE anime_id = '1234567890';
Enter fullscreen mode Exit fullscreen mode

We are done with the database section up next is the section where we are going to build our backend API.

Build a backend API with full CRUD

This section we be split into two. First, we shall create a Node.js backend and then one for Django.

Node.js backend project setup

For creating the backend and connecting it to our SQL databases we will be using Prisma for setting up relational databases.

You should still be inside the folder for anime-dashboard-app if not make sure that you are and then set up a project with Prisma using these commands:

mkdir manga-backend-express
cd manga-backend-express
npm init -y
npm i express nodemon dotenv cors prisma
Enter fullscreen mode Exit fullscreen mode

Now get the Prisma CLI running with this command:

npx prisma
Enter fullscreen mode Exit fullscreen mode

And now lets setup our Prisma project with this command:

npx prisma init
Enter fullscreen mode Exit fullscreen mode

Now open the project in your code editor, if you use VS Code and have it setup to open from the terminal you can use this command:

code .
Enter fullscreen mode Exit fullscreen mode

Ok great, next we will connect to our SQL database using Prisma.

Connecting to the SQL databases with Prisma

Use the following setups to connect to the different databases. It is only possible to connect to one of these databases and as long as you completed all the steps earlier you should have 3 databases setup with data. So choose one of the databases to connect to in the setups below:

PostgreSQL setup

The file is located at prisma/schema.prisma

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
Enter fullscreen mode Exit fullscreen mode

The .env should be in the root folder and it will hold our environment variables. This is the connection string that we will use to connect to the database. It is possible that your configuration could be different so locate the correct path:

DATABASE_URL="postgresql://postgres:@localhost:5432/manga?schema=public"
Enter fullscreen mode Exit fullscreen mode
MySQL setup

The file is located at prisma/schema.prisma

datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
Enter fullscreen mode Exit fullscreen mode

The .env should be in the root folder and it will hold our environment variables. This is the connection string that we will use to connect to the database. It is possible that your configuration could be different so locate the correct path:

DATABASE_URL="mysql://root:@localhost:3306/manga"
Enter fullscreen mode Exit fullscreen mode
SQLite setup

The file is located at prisma/schema.prisma

datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
Enter fullscreen mode Exit fullscreen mode

The .env should be in the root folder and it will hold our environment variables. This is the connection string that we will use to connect to the database. It is possible that your configuration could be different so locate the correct path:

Create a folder inside of the manga-backend directory called database and then move the SQLite database we created earlier called manga.db and put it inside of the database folder.

DATABASE_URL="file:../database/manga.db"
Enter fullscreen mode Exit fullscreen mode

We are making good progress so let's move on to the next section where we will create our database schema.

Creating the database schema

We have already created our database so we can use the commands below to create our models and schema automatically. Run these commands inside of our manga-backend project to create a schema for our database based on the tables that we created for our databases.

npx prisma db pull
Enter fullscreen mode Exit fullscreen mode
npx prisma generate
Enter fullscreen mode Exit fullscreen mode

Alternatively, we can use Prisma Migrate which means that we have to create the database models and schema manually.

Creating the Express server with CRUD endpoints

First update the .env file by adding the port and development environment variables:

PORT="8000"
ENVIRONMENT="development"
Enter fullscreen mode Exit fullscreen mode

Now create an index.js file in the root folder for manga-backend-express and add this code which will create our Express server that connects to our database. It has all of our CRUD endpoints and there is even one for INNER JOIN tables which uses raw SQL queries:

const express = require('express');
const { PrismaClient } = require('@prisma/client');
require('dotenv').config();
const cors = require('cors');
const prisma = new PrismaClient();

const app = express();

app.use(cors());
app.use(express.urlencoded({ extended: false }));
app.use(express.json());

// GET ALL

app.get('/api/anime', async (req, res) => {
  const allAnime = await prisma.anime.findMany();
  console.log(allAnime);
  res.json(allAnime);
});

// GET ONE

app.get('/api/anime/:id', async (req, res) => {
  const { id } = req.params;

  const anime = await prisma.anime.findUnique({
    where: { id: Number(id) },
  });

  res.json(anime);
  console.log(anime);
});

// POST

app.post('/api/anime', async (req, res) => {
  const { anime_id, anime_name, anime_release } = req.body;

  const result = await prisma.anime.create({
    data: {
      anime_id,
      anime_name,
      anime_release,
    },
  });

  res.json({ msg: 'Anime added' });
});

// UPDATE

app.put('/api/anime/:id', async (req, res) => {
  const { id } = req.params;
  const { anime_id, anime_name, anime_release } = req.body;

  const post = await prisma.anime.update({
    where: { id: Number(id) },

    data: {
      anime_id,
      anime_name,
      anime_release,
    },
  });

  res.json({ msg: `Anime ${id} updated` });
});

// DELETE

app.delete('/api/anime/:id', async (req, res) => {
  const { id } = req.params;

  const post = await prisma.anime.delete({
    where: { id: Number(id) },
  });

  res.json({ msg: `Anime ${id} deleted` });
});

// GET: Inner join (using raw SQL query syntax)

app.get('/api/joinedtables', async (req, res) => {
  const users = await prisma.$queryRaw`
SELECT * FROM anime
INNER JOIN anime_details
ON anime.anime_id = anime_details.anime_id;
`;

  res.json(users);
  console.log(users);
});

const port = process.env.PORT || 8000;

if (process.env.ENVIRONMENT === 'development') {
  app.listen(port, () =>
    console.log(`Server running on port ${port}, http://localhost:${port}`)
  );
}
Enter fullscreen mode Exit fullscreen mode

Lastly, add these run scripts to the package.json file so that we can run our server. We have one that uses node and another for our development server that uses nodemon so our server can auto refresh without having to stop and start the server each time we make changes.

"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
},
Enter fullscreen mode Exit fullscreen mode

Run the backend with either one of these commands and use an API testing tool like Postman to test the endpoints:

npm run start
npm run dev
Enter fullscreen mode Exit fullscreen mode

Take a look at the examples below to see the data and endpoints.

POST data endpoint

Endpoint: http://localhost:8000/api/anime/

https://res.cloudinary.com/d74fh3kw/image/upload/v1705962155/dashboard-app-post_bi5i4d.jpg

GET all data endpoint

Endpoint: http://localhost:8000/api/anime

https://res.cloudinary.com/d74fh3kw/image/upload/v1705962155/dashboard-app-get-all_azckai.jpg

GET by ID data endpoint

Endpoint: http://localhost:8000/api/anime/1

https://res.cloudinary.com/d74fh3kw/image/upload/v1705962155/dashboard-app-get-one_e4aabl.jpg

PUT data endpoint

Endpoint: http://localhost:8000/api/anime/1

https://res.cloudinary.com/d74fh3kw/image/upload/v1705962155/dashboard-app-put_a36cto.jpg

Delete data endpoint

Endpoint: http://localhost:8000/api/anime/1

https://res.cloudinary.com/d74fh3kw/image/upload/v1705962155/dashboard-app-put_a36cto.jpg

Our Express backend should be connected to one of our SQL databases. In the next section, we are going to create a Django backend.

Django backend project setup

Before we begin we should first stop our Express server from running because both servers are going to use port 8000 and only one server can run on that port, not both. So make sure that you are in the root folder for anime-dashboard-app and then create a Python virtual environment with these commands using the terminal:

You might need to use a different Python command for running these upcoming Python code scripts, it all depends on how your development environment is set up. So you might have to use python or python3 to run the commands. The same applies to using pip or pip3 for installing packages.

virtualenv manga-backend-django
source manga-backend-django/bin/activate
pip3 install django
pip3 install djangorestframework
pip install django-cors-headers
Enter fullscreen mode Exit fullscreen mode

With these commands, we are setting up a Python virtual environment and installing the packages for Django and the Django Rest Framework. Without a virtual environment, these would be installed globally on our computers.

Run this command from to root of the anime-dashboard-app folder to activate the Python virtualenv environment:

source manga-backend-django/bin/activate
Enter fullscreen mode Exit fullscreen mode

When you want to deactivate the environment you can use this command. We don't need to do that right now.

deactivate
Enter fullscreen mode Exit fullscreen mode
Connecting to the SQL databases with Django

Connecting to the different databases requires us to install drivers. Install the following drivers for the databases you intend to connect to. We only need to do this for PostgreSQL and MySQL because Django should already work with SQLite databases.

## PostgreSQL Databases
pip3 install psycopg2 psycopg2-binary

## MySQL Databases
pip3 install mysqlclient
Enter fullscreen mode Exit fullscreen mode

Now we need to create a Django application so run the commands below to do that in the same folder:

cd manga-backend-django
django-admin startproject manga
cd manga
Enter fullscreen mode Exit fullscreen mode

Now open the manga-backend-django project in your code editor if you have not done so already because we are going to be adding code to the files in our Django codebase.

Next, we need to connect to our database so use the example database connection examples below as a reference for each database and configure them for your ones. Open the manga/manga/settings.py file in your project and find the databases section. The connection object should be very similar to the one that we used for our Express app with Prisma.

PostgreSQL
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'manga',
'USER': 'postgres',
'PASSWORD': '',
'HOST': 'localhost',
'PORT': '5432',
}
}
Enter fullscreen mode Exit fullscreen mode
MySQL
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'manga',
'USER': 'root',
'PASSWORD': '',
'HOST': 'localhost',
'PORT': '3306',
}
}
Enter fullscreen mode Exit fullscreen mode
SQLite

When setting it up for SQLite databases we have to ensure that our SQLite database is in the correct folder. The base directory is the root folder for the project so for example manga-backend-django/database. You can just copy the database folder from the manga-backend-express project and put it in the root folder for this project.

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'database/db.manga',
}
}
Enter fullscreen mode Exit fullscreen mode

Great now let's work on this next step which will be to do the migration and create a super user for the database.

Doing a Python migration and creating a super user for our database

Make sure that you are inside the manga folder with the manage.py file and run the following commands. We will use Python migration to set all of the tables for our database:

python3 manage.py migrate
Enter fullscreen mode Exit fullscreen mode

Ok now let's create a super user for the database so that we can access it. Run this command and go through the steps. You are required to have a Username and Password at least:

python3 manage.py createsuperuser
Enter fullscreen mode Exit fullscreen mode

Right with that setup completed let's finish it off by creating a Django server with CRUD endpoints.

Creating the Django server with CRUD endpoints

We first need to create an app called API so run this code from the same folder with the manage.py file in it to create it:

django-admin startapp api
Enter fullscreen mode Exit fullscreen mode

Now configure the Django Rest Framework in settings.py by adding these packages to theINSTALLED_APPS = [] array:

'api.apps.ApiConfig',
'rest_framework',
'corsheaders',
Enter fullscreen mode Exit fullscreen mode

To avoid CORS errors on the front end we will need to add some more configurations to our settings.py file. First, add these modules to the existing MIDDLEWARE array in the file:

'django.middleware.common.CommonMiddleware',
'corsheaders.middleware.CorsMiddleware',
Enter fullscreen mode Exit fullscreen mode

Then add these CORS configurations somewhere in the file:

CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",  # Add your frontend URL here
]

CORS_ALLOW_METHODS = [
    'DELETE',
    'GET',
    'OPTIONS',
    'PATCH',
    'POST',
    'PUT',
]

CORS_ALLOW_HEADERS = [
    'accept',
    'accept-encoding',
    'authorization',
    'content-type',
    'dnt',
    'origin',
    'user-agent',
    'x-csrftoken',
    'x-requested-with',
]
Enter fullscreen mode Exit fullscreen mode

Now we need to create our database models so add this code to the api/models.py file:

from django.db import models

class Anime(models.Model):
    id = models.AutoField(primary_key=True)
    anime_id = models.CharField(max_length=10, null=True)
    anime_name = models.CharField(max_length=100, null=True)
    anime_release = models.CharField(max_length=10, null=True)

    def __str__(self):
        return self.anime_name

class AnimeDetails(models.Model):
    id = models.AutoField(primary_key=True)
    anime_id = models.CharField(max_length=10, null=True)
    anime_genre = models.CharField(max_length=50, null=True)
    anime_rating = models.IntegerField(null=True)

    def __str__(self):
        return self.anime_id

Enter fullscreen mode Exit fullscreen mode

After defining the models, run the following commands to create the necessary database tables:

python manage.py makemigrations
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

This will create "api_anime" and "api_animedetails" tables in your database along with a few others. Existing tables for "anime" and "anime_details" already exist so it will create new ones for the models. You can see what that looks like inside of Azure Data Studio here:

https://res.cloudinary.com/d74fh3kw/image/upload/v1706032414/django-db-tables_otkmvj.jpg

The next step is to create the Serializers and API views. Serializers transform complicated data, like querysets and model instances, to native Python datatypes that can be readily displayed as JSON, XML, or other content types. Create a file at api/serializers.py and add this code:

from rest_framework import serializers
from .models import Anime, AnimeDetails


class AnimeSerializer(serializers.ModelSerializer):
    class Meta:
        model = Anime
        fields = '__all__'

class AnimeDetailsSerializer(serializers.ModelSerializer):
    class Meta:
        model = AnimeDetails
        fields = '__all__'

Enter fullscreen mode Exit fullscreen mode

And now add this code to the api/views.py file which is where our database data will be returned:

from django.shortcuts import render
# Create your views here.
from rest_framework import viewsets
from .models import Anime, AnimeDetails
from .serializers import AnimeSerializer, AnimeDetailsSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import connection


class AnimeViewSet(viewsets.ModelViewSet):
    queryset = Anime.objects.all()
    serializer_class = AnimeSerializer


class AnimeDetailsViewSet(viewsets.ModelViewSet):
    queryset = AnimeDetails.objects.all()
    serializer_class = AnimeDetailsSerializer


class JoinedTablesView(APIView):
    def get(self, request):
        try:
            with connection.cursor() as cursor:
                cursor.execute("""
                SELECT * FROM api_anime
                INNER JOIN api_animedetails ON api_anime.anime_id = api_animedetails.anime_id;
                """)
                columns = [col[0] for col in cursor.description]
                data = [dict(zip(columns, row)) for row in cursor.fetchall()]
                return Response(data)

        except OperationalError as e:
            # Log the exception or handle it appropriately
            return Response({"error": f"Database error: {e}"}, status=500)

Enter fullscreen mode Exit fullscreen mode

Ok, next we will create a file for api/urls.py with this code which is where we will be doing the routing for our URL endpoints:

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import AnimeViewSet, AnimeDetailsViewSet, JoinedTablesView


router = DefaultRouter()
router.register(r"anime", AnimeViewSet)
router.register(r"anime-details", AnimeDetailsViewSet)


urlpatterns = [
    path("", include(router.urls)),
    path("joinedtables/", JoinedTablesView.as_view(), name="joined-tables"),
]

Enter fullscreen mode Exit fullscreen mode

Finally, replace the code in the manga/urls.py file with this code which contains our root endpoints:

from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("api.urls")),  # Include the 'api' app's URLs
]
Enter fullscreen mode Exit fullscreen mode

After defining the views and URLs, we must run the following commands to create the necessary database tables. If you get any errors in the console they are most likely related to Python indentation, so try to format the files which are causing problems:

python3 manage.py makemigrations
python3 manage.py migrate
Enter fullscreen mode Exit fullscreen mode

That's it we should be done with this section so, run the Django development server with the code below:

python3 manage.py runserver
Enter fullscreen mode Exit fullscreen mode

The Django API should be accessible at http://127.0.0.1:8000/api/anime/, http://127.0.0.1:8000/api/anime-details/ and http://127.0.0.1:8000/api/joinedtables/ for performing CRUD operations on your Anime and AnimeDetails models. As you can see Django has a built-in API testing framework called the Django Rest Framework so you don't need to use an external API testing tool like Postman.

We can also access the database tables in our Database Management System (Azure Data Studio) and perform CRUD on them. Just remember that in our Django application, the database tables will be called api_anime and api_animedetails.

Note: If you see the errors below when using your API testing with the Django backend then you need to add a trailing slash at the end of your URL endpoint.

RuntimeError: You called this URL via POST, but the URL doesn't end in a slash and you have APPEND_SLASH set. Django can't redirect to the slash URL while maintaining POST data. Change your form to point to localhost:8000/api/anime/ (note the trailing slash), or set APPEND_SLASH=False in your Django settings.

RuntimeError: You called this URL via PUT, but the URL doesn't end in a slash and you have APPEND_SLASH set. Django can't redirect to the slash URL while maintaining PUT data. Change your form to point to localhost:8000/api/anime/1/ (note the trailing slash), or set APPEND_SLASH=False in your Django settings.

If you encounter any new errors then they are most likely caused by incorrect Python indentation. We have completed this section, now let's work on the last section which is where we are going to build the frontend.

Building the frontend

Ok, this time around let's just assume that a frontend developer has picked up a ticket from Jira for creating the frontend user interface. The first stage would be to scaffold a project using a framework in this case it will be Next.js.

In this example, we will integrate a chart library on the frontend that connects to the backend which has full CRUD functionality.

Here is a quick run-through of what we will be doing:

  1. Create a React project using Next.js
  2. Setup Jest, and React Testing Library with an option for normal tests and testing coverage
  3. Build components and use a TDD workflow

Now that we have an overview of the tasks let's begin.

Create a React project using Next.js

The first thing we need to do is use the command line to get back to the root folder for our project which is anime-dashboard-app. Now create a Next.js project and make sure that you select Yes for Tailwind CSS because we will need it later. You can use the settings I used here:

npx create-next-app manga-client
Enter fullscreen mode Exit fullscreen mode

βœ” Would you like to use TypeScript? … No / Yes
βœ” Would you like to use ESLint? … No / Yes
βœ” Would you like to use Tailwind CSS? … No / Yes
βœ” Would you like to use src/ directory? … No / Yes
βœ” Would you like to use App Router? (recommended) … No / Yes
βœ” Would you like to customize the default import alias (@/*)? … No / Yes

Let's quickly discuss the topic of separation of concerns. In computer science, separation of concerns is a design idea that divides a computer program into different parts. Each part handles a distinct issue, a collection of data that impacts the coding of a computer program. Taking this into consideration we are going to ensure that our folders are well defined and that there is a separate folder which holds all of our tests.

We will now set up the project folder structure, for our components, custom hooks and test files. Every component should have a test. Make sure that you run this script from the root of the manga-client folder in your terminal:

mkdir __tests__
touch __tests__/FormDelete.test.js
touch __tests__/FormPostUpdate.test.js
touch jest.config.mjs
cd src/app
mkdir components hooks
mkdir components/FormDelete
mkdir components/FormPostUpdate
cd components
touch FormDelete/FormDelete.js
touch FormPostUpdate/FormPostUpdate.js
cd ..
touch hooks/useFetch.js hooks/useFetchSWR.js hooks/usePost.js hooks/useUpdate.js hooks/useDelete.js
cd ../..
Enter fullscreen mode Exit fullscreen mode

Next, we will install the dependencies for our project so install the required packages with these commands:

npm i --save-dev jest @types/jest @testing-library/jest-dom @testing-library/react @testing-library/user-event jest-environment-jsdom react-test-renderer swr
Enter fullscreen mode Exit fullscreen mode

Ok now if your project is not open in a code editor do so now. The next step is to add these Jest run scripts to the package.json file which we will use for testing our components:

"test": "jest --watchAll",
"coverage": "jest --collect-coverage --collectCoverageFrom='./src/app/components/**' --coveragePathIgnorePatterns='\\.snap$'"
Enter fullscreen mode Exit fullscreen mode

With our test scripts set up now, we need to add this code to the jest.config.mjs file which we created so that we have a working Jest configuration ready for when we want to run some tests:

import nextJest from 'next/jest.js';

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
});
// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config = {
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jest-environment-jsdom',
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: -10,
    },
  },
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async

export default createJestConfig(config);
Enter fullscreen mode Exit fullscreen mode

These test run scripts should be working. If you run them now the tests will fail because we have not added any code to our test files yet.

npm run test
npm run coverage
Enter fullscreen mode Exit fullscreen mode

Ok, our frontend project has been set up all that is left is to build components and tests and then our anime dashboard application will be complete!

Build components

Starting with our components we will begin by adding the upcoming code to our component files.

Up first will be our form delete component which is inside the components folder. It is located at FormDelete/FormDelete.js. This component is self-explanatory it will be used for deleting animes from our database.

import { useState } from 'react';
import { useDelete } from '@/app/hooks/useDelete';

export default function FormDelete() {
  const [id, setId] = useState('');
  const { deleteRequest } = useDelete();
  const handleDeleteForm = async (e) => {
    e.preventDefault();
    if (id === '') {
      console.log('Form needs and id to be submitted');
    } else {
      try {
        deleteRequest(`http://127.0.0.1:8000/api/anime/${id}/`);
        console.log(`Anime ${id} deleted`);
        setId('');
      } catch (error) {
        console.log(error);
      }
    }
  };

  return (
    <>
      <form onSubmit={(e) => handleDeleteForm(e)}>
        <div className="bg-sky-200 flex flex-wrap items-center mb-2">
          <label className="p-2 w-24">ID</label>
          <input
            type="text"
            value={id}
            onChange={(e) => setId(e.target.value)}
            className="grow p-2"
            required
          />
        </div>
        <div>
          <button
            type="submit"
            className="bg-sky-600 p-2 text-white cursor-pointer font-bold rounded-lg"
          >
            Submit
          </button>
        </div>
      </form>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next is the form for adding and updating animes in the database and the file can be located at FormPostUpdate/FormPostUpdate.js. Add the code to the file:

import { useState } from 'react';
import { usePost } from '../../hooks/usePost';
import { useUpdate } from '../../hooks/useUpdate';

export default function FormPostUpdate({ crudType }) {
  const [id, setId] = useState('');
  const [animeId, setAnimeId] = useState('');
  const [animeName, setAnimeName] = useState('');
  const [animeRelease, setAnimeRelease] = useState('');
  const { postRequest } = usePost();
  const { updateRequest } = useUpdate();
  const handlePostUpdateForm = async (e, type) => {
    e.preventDefault();

    if (animeId === '' || animeName === '' || animeRelease === '') {
      console.log('Form needs data to be submitted');
    } else {
      try {
        const anime = {
          anime_id: animeId,
          anime_name: animeName,
          anime_release: animeRelease,
        };
        if (type === 'posted') {
          postRequest('http://127.0.0.1:8000/api/anime/', anime);
          console.log(`Anime ${type}`);
          setId('');
          setAnimeId('');
          setAnimeName('');
          setAnimeRelease('');
        } else if (type === 'updated' && id !== '') {
          updateRequest(`http://127.0.0.1:8000/api/anime/${id}/`, anime);
          console.log(`Anime ${type}`);
          setId('');
          setAnimeId('');
          setAnimeName('');
          setAnimeRelease('');
        }
      } catch (error) {
        console.log(error);
      }
    }
  };

  return (
    <>
      <form onSubmit={(e) => handlePostUpdateForm(e, crudType)}>
        {crudType === 'updated' ? (
          <div className="bg-sky-200 flex flex-wrap items-center mb-2">
            <label className="p-2 w-24">ID</label>
            <input
              type="text"
              value={id}
              onChange={(e) => setId(e.target.value)}
              className="grow p-2"
              required
            />
          </div>
        ) : (
          <></>
        )}
        <div className="bg-sky-200 flex flex-wrap items-center mb-2">
          <label className="p-2 w-24">Anime ID</label>
          <input
            type="text"
            value={animeId}
            onChange={(e) => setAnimeId(e.target.value)}
            className="grow p-2"
            required
          />
        </div>

        <div className="bg-sky-200 flex flex-wrap items-center mb-2">
          <label className="p-2 w-24">Name</label>
          <input
            type="text"
            value={animeName}
            onChange={(e) => setAnimeName(e.target.value)}
            className="grow p-2"
            required
          />
        </div>
        <div className="bg-sky-200 flex flex-wrap items-center mb-2">
          <label className="p-2 w-24">Release</label>
          <input
            type="text"
            value={animeRelease}
            onChange={(e) => setAnimeRelease(e.target.value)}
            className="grow p-2"
            required
          />
        </div>
        <div>
          <button
            type="submit"
            className="bg-sky-600 p-2 text-white cursor-pointer font-bold rounded-lg"
          >
            Submit
          </button>
        </div>
      </form>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next, let's do the Page.js file which is the main file for our entire application. This is where all of our components will reside so replace all of the code with this code here:

'use client';
import FormPostUpdate from './components/FormPostUpdate/FormPostUpdate';
import FormDelete from '../app/components/FormDelete/FormDelete';
import { useFetch } from './hooks/useFetch';
import { useFetchSWR } from './hooks/useFetchSWR';
export default function Home() {
  // Uncomment the code below and comment out the "useFetch" code if your want to use SWR for data fetching --> https://swr.vercel.app/docs/with-nextjs
  // const { data, error, isLoading } = useFetchSWR(
  // 'http://127.0.0.1:8000/api/anime/'
  // );
  // Uses the Fetch API for data fetching
  const { data, error, isLoading } = useFetch(
    'http://127.0.0.1:8000/api/anime/'
  );
  if (error) return <div>An error has occurred.</div>;
  if (isLoading) return <div>Loading...</div>;
  console.log(data);

  return (
    <>
      <div className="container mx-auto mt-10">
        <div className="bg-sky-300 rounded-lg p-4 mb-4">
          <h1 className="text-xl mb-4">POST: Add Anime Form</h1>
          <FormPostUpdate crudType="posted" />
        </div>
        <div className="bg-sky-300 rounded-lg p-4 mb-4">
          <h1 className="text-xl mb-4">UPDATE: Update Anime Form</h1>
          <p className="bg-yellow-100 p-4">
            Select an ID from the list. You can change the data for Anime ID,
            Name and Release.
          </p>
          <FormPostUpdate crudType="updated" />
        </div>
        <div className="bg-sky-300 rounded-lg p-4 mb-4">
          <h1 className="text-xl mb-4">DELETE: Delete Anime Form</h1>
          <FormDelete />
        </div>
        <div className="bg-sky-700 rounded-lg p-4 mb-4 text-white">
          <h1 className="text-xl">GET: Anime Data List</h1>
          {data.map((anime) => (
            <div key={anime.id}>
              <ul className="bg-sky-800 mb-4 p-4">
                <li>
                  <h1 className="text-lg font-bold">{anime.anime_name}</h1>
                </li>
                <li>ID: {anime.id}</li>
                <li>Anime ID: {anime.anime_id}</li>
                <li>Anime Release Year: {anime.anime_release}</li>
              </ul>
            </div>
          ))}
        </div>
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Our main components are complete in the next section we will create some test files.

Setting up our test files

There are going to be two test files for the two form components we made. These test files do not have full test coverage they only test to see if the component has rendered without crashing. You can add more tests if you want to. Our test file is located in the root folder for this project and is named __tests__.

Start by adding this test to the form delete file located in FormDelete.test.js.

import { render } from '@testing-library/react';
import FormDelete from '../src/app/components/FormDelete/FormDelete';

describe('component renders', () => {
  test('component does not crash', () => {
    render(<FormDelete />);
  });
});
Enter fullscreen mode Exit fullscreen mode

And lastly, add this code to the form post update file located in FormPostUpdate.test.js.

import { render } from '@testing-library/react';
import FormPostUpdate from '../src/app/components/FormPostUpdate/FormPostUpdate';

describe('Form component', () => {
  test('renders without crashiing', () => {
    render(<FormPostUpdate />);
  });
});
Enter fullscreen mode Exit fullscreen mode

Let's now create our reusable hooks in the next section for all of our backend CRUD requests which we created earlier.

Creating our custom hooks

The first custom hook we create will be for getting data from our backend.

GET Requests

Two custom hooks are available in this section but we only need to use one of them. This version uses the Fetch API and the file is located in hooks/useFetch.js so add this code to the file. It also has a polling setup which means that it can automatically fetch data periodically from our API so that we do not have to manually refresh the page after performing a CRUD action.

It is just a simple function that calls the fetch data function every 2 seconds and can be configured to increase or decrease the time. This is an easy-to-implement feature that does not require a third-party library like WebSockets. Of course, using WebSockets would be a much better implementation but for this simple demo application, it's fine.

import { useState, useEffect } from 'react';

export function useFetch(url) {
  const [data, setData] = useState([]);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const json = await fetch(url).then((r) => r.json());
        setIsLoading(false);
        setData(json);
      } catch (error) {
        setError(error);
        setIsLoading(false);
      }
    };
    fetchData();

    // Polling so that the chart with the most recent changes from the database updates without having to do a page reload anytime a CRUD request is sent to the database
    const pollInterval = setInterval(() => {
      fetchData(); // Fetch data periodically
    }, 2000); // Poll every 2 seconds (adjust as needed)

    // Clear the interval when the component unmounts
    return () => {
      clearInterval(pollInterval);
    };
  }, [url]);

  return { data, error, isLoading };
}
Enter fullscreen mode Exit fullscreen mode

This alternative version uses SWR and the file is located in hooks/useFetchSWR so add the code to it. You can choose which version you want to use by looking at the comments in the main page.js file.

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

export function useFetchSWR(url) {
  const { data, error, isLoading } = useSWR(url, fetcher);
  return { data, error, isLoading };
}
Enter fullscreen mode Exit fullscreen mode

Ok, moving on it's time to work on the post-request hook.

POST Requests

The file is located in hooks/usePost.js and this hook is used for adding animes to our database so add this code to the file:

import { useState } from 'react';

export function usePost() {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [response, setResponse] = useState(null);
  const postRequest = async (url, formData) => {
    setIsLoading(true);
    setError(null);
    setResponse(null);

    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData),
      });

      const responseData = await response.json();

      if (response.ok) {
        setResponse(responseData);
      } else {
        setError(responseData);
      }
    } catch (error) {
      setError(error);
    } finally {
      setIsLoading(false);
    }
  };

  return { isLoading, error, response, postRequest };
}
Enter fullscreen mode Exit fullscreen mode

Right, let's move on to the update hook next.

UPDATE Requests

We can find the file in hooks/useUpdate.js this hook is for updating our data in the database so add this code to the file:

import { useState } from 'react';

export function useUpdate() {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [response, setResponse] = useState(null);

  const updateRequest = async (url, formData) => {
    setIsLoading(true);
    setError(null);
    setResponse(null);

    try {
      const response = await fetch(url, {
        method: 'PUT',

        headers: {
          'Content-Type': 'application/json',
        },

        body: JSON.stringify(formData),
      });

      const responseData = await response.json();

      if (response.ok) {
        setResponse(responseData);
      } else {
        setError(responseData);
      }
    } catch (error) {
      setError(error);
    } finally {
      setIsLoading(false);
    }
  };

  return { isLoading, error, response, updateRequest };
}
Enter fullscreen mode Exit fullscreen mode

And lastly lets do our delete hook.

DELETE Requests

The file is located in hooks/useDelete.js and this hook is for deleting data in the database so add this code below to the file.

import { useState } from 'react';

export function useDelete() {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [response, setResponse] = useState(null);

  const deleteRequest = async (url) => {
    setIsLoading(true);
    setError(null);
    setResponse(null);

    try {
      const response = await fetch(url, {
        method: 'DELETE',
        headers: {
          'Content-Type': 'application/json',
        },
      });

      const responseData = await response.json();

      if (response.ok) {
        setResponse(responseData);
      } else {
        setError(responseData);
      }
    } catch (error) {
      setError(error);
    } finally {
      setIsLoading(false);
    }
  };

  return { isLoading, error, response, deleteRequest };
}
Enter fullscreen mode Exit fullscreen mode

Ok good, there is just one more step to go. Find the globals.css file which should be inside of the src/app folder in your Next.js project. Just delete all of the CSS in there apart from the tailwind CSS imports at the top of the file. So it should look like this:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

We have to do this so that the styles in there do not conflict with the ones we created for this Tailwind CSS design.

Now let's see if it's all working, first you need to make sure that the database is up and running, and that one of the backends (Node or Django) is running on port 8000. Our frontend needs to connect to the backend so obviously it needs to be running first. Use this run script to start the app afterwards:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Our app should look like the design here:

https://res.cloudinary.com/d74fh3kw/image/upload/v1706118323/dashboard-app-no-chart_adlyf5.jpg

And use these for the tests:

npm run test
npm run coverage
Enter fullscreen mode Exit fullscreen mode

The tests should all pass although you will see that the test coverage score is low. That is because we did not fully test all of the components if you want you can play around with the files and try to get the test coverage up.

All that's left is to add a chart to our application and then it truly will be complete! Let's move on to the final section and get this done!

Integrating a chart library with full CRUD

We now have working backends for Node and Django which connect to SQL databases. We can do full CRUD requests to the backend on the frontend using the forms on the page. Let's now integrate a chart that shows the data from the backend with full CRUD functionality.

The chart library we will use for this example will be Highcharts so first install the package into our Next.js application:

npm install highcharts highcharts-react-official
Enter fullscreen mode Exit fullscreen mode

Up next we will need to, create a folder, component and test for the chart in the components folder using the mentioned here. Now following our current project structure start by creating a folder and file for our Chart.js component. The file directory should be as follows src/app/components/Chart/Chart.js. Add this code to our Chart.js file and like before the name is self-explanatory this component will be used for displaying our chart along with its data on the page.

import { useState, useEffect } from 'react';
import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import { useFetch } from '@/app/hooks/useFetch';

export default function Chart() {
  const { error, isLoading, data } = useFetch(
    'http://127.0.0.1:8000/api/anime/'
  );

  const [animeDate, setAnimeDate] = useState('');
  const [filterAnimeDate, setFilterAnimeDate] = useState([]);
  const [hoverData, setHoverData] = useState(null);
  const [chartOptions, setChartOptions] = useState({
    xAxis: {
      categories: [],
    },
    title: {
      text: 'Animes',
      align: 'center',
    },
    series: [{ data: [] }],
    plotOptions: {
      series: {
        point: {
          events: {
            mouseOver(e) {
              setHoverData(e.target.category);
            },
          },
        },
      },
    },
  });

  useEffect(() => {
    setChartOptions({
      xAxis: {
        categories: data.map((item) => item.anime_name),
      },
      series: [{ data: data.map((item) => parseInt(item.anime_id)) }],
      plotOptions: {
        series: {
          point: {
            events: {
              mouseOver(e) {
                setHoverData(e.target.category);
              },
            },
          },
        },
      },
    });
  }, [data]);

  if (error) return <div>Error</div>;
  if (isLoading) return <div>Loading...</div>;

  const handleFilterAnime = (e) => {
    e.preventDefault();

    const filterDate = data.filter((a) => a.anime_release === animeDate);
    console.log(filterDate);
    setFilterAnimeDate(filterDate);

    setAnimeDate('');
  };

  return (
    <>
      <div>
        <HighchartsReact highcharts={Highcharts} options={chartOptions} />
        <p className="bg-green-300 p-4 mb-4 mt-4 rounded-lg">
          Hovering over: <span className="font-bold">{hoverData}</span>
        </p>
      </div>
      <form onSubmit={handleFilterAnime}>
        <div className="bg-sky-300 rounded-lg p-4 mb-4">
          <div>
            <h1 className="text-xl mb-4">Filter by release year</h1>
            <div className="bg-sky-200 flex flex-wrap items-center mb-2">
              <label className="p-2 w-24">Date:</label>
              <input
                type="text"
                value={animeDate}
                onChange={(e) => setAnimeDate(e.target.value)}
                className="grow p-2"
                required
              />
            </div>
            <div>
              <button
                type="submit"
                className="bg-sky-600 p-2 text-white cursor-pointer font-bold rounded-lg"
              >
                Submit
              </button>
            </div>
          </div>
        </div>
      </form>

      <div className="bg-green-600 p-4 rounded-lg">
        <h1 className="text-xl text-white">Release Year</h1>
        {filterAnimeDate === 0 ? (
          <></>
        ) : (
          <>
            {filterAnimeDate.map((anime) => (
              <div key={anime.id} className="mt-4 text-white">
                <ul className="bg-green-700 mb-4 p-4">
                  <li>
                    <h1 className="text-lg font-bold">{anime.anime_name}</h1>
                  </li>
                  <li>ID: {anime.id}</li>
                  <li>Anime ID: {anime.anime_id}</li>
                  <li>Anime Release Year: {anime.anime_release}</li>
                </ul>
              </div>
            ))}
          </>
        )}
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Ok almost done now create a test file for Chart.test.js inside of the __tests__ folder and add this code for the chart test file:

import { render } from '@testing-library/react';
import Chart from '../src/app/components/Chart/Chart';

describe('it renders', () => {
  test('Chart component loads', () => {
    render(<Chart />);
  });
});
Enter fullscreen mode Exit fullscreen mode

Finally, update the Page.js file with this code so that our chart component can now be displayed on the page and we are done!

'use client';
import FormPostUpdate from './components/FormPostUpdate/FormPostUpdate';
import FormDelete from '../app/components/FormDelete/FormDelete';
import { useFetch } from './hooks/useFetch';
import { useFetchSWR } from './hooks/useFetchSWR';
import Chart from './components/Chart/Chart';

export default function Home() {
  // Uncomment the code below and comment out the "useFetch" code if your want to use SWR for data fetching --> https://swr.vercel.app/docs/with-nextjs
  // const { data, error, isLoading } = useFetchSWR(
  // 'http://127.0.0.1:8000/api/anime/'
  // );
  // Uses the Fetch API for data fetching
  const { data, error, isLoading } = useFetch(
    'http://127.0.0.1:8000/api/anime/'
  );

  if (error) return <div>An error has occurred.</div>;
  if (isLoading) return <div>Loading...</div>;
  console.log(data);

  return (
    <>
      <div className="container mx-auto mt-10 grid gap-4 lg:grid-cols-2 md:grid-cols-2 sm:grid-cols-1">
        <div>
          <div className="bg-sky-300 rounded-lg p-4 mb-4">
            <h1 className="text-xl mb-4">POST: Add Anime Form</h1>
            <FormPostUpdate crudType="posted" />
          </div>
          <div className="bg-sky-300 rounded-lg p-4 mb-4">
            <h1 className="text-xl mb-4">UPDATE: Update Anime Form</h1>
            <p className="bg-yellow-100 p-4">
              Select an ID from the list. You can change the data for Anime ID,
              Name and Release.
            </p>
            <FormPostUpdate crudType="updated" />
          </div>
          <div className="bg-sky-300 rounded-lg p-4 mb-4">
            <h1 className="text-xl mb-4">DELETE: Delete Anime Form</h1>
            <FormDelete />
          </div>
          <div className="bg-sky-700 rounded-lg p-4 mb-4 text-white">
            <h1 className="text-xl">GET: Anime Data List</h1>
            {data.map((anime) => (
              <div key={anime.id}>
                <ul className="bg-sky-800 mb-4 p-4">
                  <li>
                    <h1 className="text-lg font-bold">{anime.anime_name}</h1>
                  </li>
                  <li>ID: {anime.id}</li>
                  <li>Anime ID: {anime.anime_id}</li>
                  <li>Anime Release Year: {anime.anime_release}</li>
                </ul>
              </div>
            ))}
          </div>
        </div>
        <div>
          <Chart />
        </div>
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Alright! We should have a working chart with full CRUD functionality that has real-time updates and another form for filtering the anime by dates and the data is shown on the page. You might need to reload the page so it updates with the new changes we made before it starts working properly. All 3 tests should be passing and this is what the final design looks like:

https://res.cloudinary.com/d74fh3kw/image/upload/v1706039009/dashboard-app_ckgxzs.jpg

Congratulations you have just built a full-stack dashboard application and completed this project πŸŽ‰

Conclusion

So today we had a run-through of what it could be like to work as a developer. We had a brief introduction to Agile and working with Jira. And we managed to build two backends one using Node.js and Express and the other using Python and Django. We used our API testing tool (in this case Postman) to perform CRUD (Create, Read, Update, Delete) requests. Then we connected the backends to our frontend which we created using Next.js. This allowed us to see the CRUD functionality working on the frontend. Lastly, we implemented a chart that shows the data in our database completing our dashboard application.

Building software can require a lot of work and phases and what we saw today was just a small snapshot of a developer's workflow. There are many more areas to consider like having frequent team meetings, and code reviews every time you make changes etc... To improve your knowledge in this area even more you could try setting up a Jira or alternative workflow with a full CI/CD pipeline as this can be good practice for beginners who have yet to work professionally in a company team. In closing, there are infinite ways for a developer and teams to work and this is just one example.

Top comments (20)

Collapse
 
michaeltharrington profile image
Michael Tharrington

Wow, this is a super thorough and well-written post, Andrew. Good to see your writing back on DEV! πŸ™Œ

Collapse
 
andrewbaisden profile image
Andrew Baisden

Thanks for the great feedback! Yeah, last year I was really busy writing a book so I had less time for articles. This year I will be back to writing loads of articles again πŸ”₯

Collapse
 
madza profile image
Madza

A great and detailed tutorial for full-stack app using NextJS. Wrote a similar tutorial yesterday as well, though using CMS as a backend! πŸ‘πŸ’―

Collapse
 
andrewbaisden profile image
Andrew Baisden

Thanks for being the first to comment @madza glad you liked it πŸ™ I will check out your tutorial next. This is the first article I have published this year. Plenty more to come 😁

Collapse
 
madza profile image
Madza

My pleasure and great job on it! πŸ”₯πŸ”₯

Collapse
 
hassanndam profile image
hassan

Hi Andrew, I followed your tutorial, it's excellent and very detailed. It really helped me to understand how to set up a RESTFULL API and above all to structure my project in the form of layers (back, API, DB & front).

I was having a lot of trouble with this but now it's much clearer in my head. I followed the tutorial from A to Z in 3 days and it was OK.

I just find that it's really designed for intermediate devs, a beginner will have difficulty understanding certain concepts and moving from one file to another.

I have two questions:

1) Why did you create two new tables api_anime & api_animedetails please? Why didn't you just use the anime & anime_details tables? What is the consistency between these two and in which part of Django can I see this please?

2) I've encountered the problem with the "Release Year" display, can you please look at my code and tell me how to fix it?

It's all here: github.com/HassanNdam/Application-...

Thanks a lot,

Sincerely

Collapse
 
andrewbaisden profile image
Andrew Baisden

Thanks for the constructive feedback!

1) Great question so it was the Django script which automatically generated these tables and gave them that naming convention. You can find it in the article in this section After defining the models, run the following commands to create the necessary database tables:. I guess it could be possible to use the existing tables if you play around with the code somewhere.

2) I will check it out and let you know

Collapse
 
andrewbaisden profile image
Andrew Baisden

@hassanndam did you resolve it? What is the problem with the release year display? I tried the code on your repo and it worked. The release year filter will only filter by dates if they are present in the Anime Data List. I did not code any error handling for it so if you try to filter by a date that does not exist then it just won't show anything.

Collapse
 
hassanndam profile image
hassan

Thank you @andrewbaisden.

I will check and come back to you.

You can check all my commits and tell me.

With thanks.

Collapse
 
sosocrypto profile image
sosocrypto

Well written post. Learnt a lot!!!

Collapse
 
andrewbaisden profile image
Andrew Baisden

Thanks it's always good to have positive feedback!

Collapse
 
nellysunday profile image
Sunday

Wow.... Wonderful

Collapse
 
andrewbaisden profile image
Andrew Baisden

Thanks it took a long time to write and it shows 😁

Collapse
 
ngud0119 profile image
NGUD-0119: ACE

Awesome.

Collapse
 
andrewbaisden profile image
Andrew Baisden

Thank you!

Collapse
 
ayoubroot profile image
ayoub benayache

Really super post, I give 10/10, we appreciate your time to write it.

Collapse
 
andrewbaisden profile image
Andrew Baisden

Thanks! I agree it took me a long time to write its one of my longest as well.

Collapse
 
alienretro profile image
John

Nice overview

Collapse
 
andrewbaisden profile image
Andrew Baisden

Glad to hear that, I think its one of my most useful articles.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.