Tutorial Fullstack: React Typescript + Django Rest framework + PostgreSQL

react typescript laravel mysql

En este tutorial, le mostraré cómo crear un proyecto fullstack de React + Django + PostgreSQL con una aplicación CRUD. El servidor back-end usaremos Django Rest Framework para API REST, el front-end con una aplicación cliente React Typescript, Axios y Tailwind y conectaremos a la BD de PostgreSQL.

In this tutorial, I’ll show you how to create a fullstack React + Django+ PostgreSQL project with a CRUD application. The back-end server we will use Django Rest Framework for REST API, the front-end with a React Typescript client application, Axios and Tailwind and we will connect to the PostgreSQL DB.

Architecture Fullstack


React Typescipt
https://create-react-app.dev/docs/adding-typescript/

Tailwind CC
https://tailwindcss.com/docs/guides/create-react-app

Frontend

1 – Create app

npx create-react-app app-react-typescript --template typescript
npm install sweetalert2

cd app-react-typescript
npm start

2 – Tailwind CSS

npm install -D tailwindcss
npx tailwindcss init
Configure tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

3 – Interfaces

src\interfaces\Person.ts

export interface IPerson {
    id?: number | null,
    name: string,
    address: string,
    phone: number,
    createdAt: Date | null,
    updatedAt: Date | null
}

export class Person implements IPerson {
    public id: null; 
    public name: string;
    public address: string;
    public phone: number; 
    public createdAt!: Date | null;
    public updatedAt!: Date | null;

    constructor(){
        this.id = null; 
        this.name = "";
        this.address = "";
        this.phone = 0; 
        this.createdAt = null;
        this.updatedAt = null;
    }
}

export const { setData, setPersons } = personSlice.actions

export default personSlice.reducer

4 – Services

Axios

npm install axios

src\configs\axios.ts

import axios from 'axios'

export const api = axios.create({
    baseURL: 'http://localhost:8080/api/',
}); 

export const headerAPI = {
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
  }

src\services\person.service.ts

import { api, headerAPI } from "../configs/axios";
import { IPerson } from '../interfaces/Person';


export class PersonService {

    private apiURL = "v1/persons";

    public async getAll() {
        try {
            console.log("Consulto")
            const response = await api.get<IPerson[]>(`${this.apiURL}`)
            return await response.data            
        } catch (error) {
            console.log(error)
            throw error;
        }
    }

    public async post(data:IPerson) {
        try {
            const response = await api.post<IPerson>(`${this.apiURL}`, data, headerAPI)
            return await response.data            
        } catch (error) {
            console.log(error)
            throw error;
        }
    }

    public async getById(id:number){
        try {
            const response = await api.get<IPerson>(`${this.apiURL}/${id}`, headerAPI)
            const data: IPerson = response.data 
            return data          
        } catch (error) {
            console.log(error)
            throw error;
        }
    }

    public async put(data:IPerson) {
        try {
            const response = await api.put<IPerson>(`${this.apiURL}/${data.id}`, data, headerAPI)
            return await response.data            
        } catch (error) {
            console.log(error)
            throw error;
        }
    }

    public async delete(data:IPerson) {
        try {
            const response = await api.delete(`${this.apiURL}/${data.id}`, headerAPI)
            return await response.data            
        } catch (error) {
            console.log(error)
            throw error;
        }
    }

}

5 – Redux toolkit

npm install @reduxjs/toolkit react-redux

src\features\person\personSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { IPerson, Person } from '../../interfaces/Person';

export interface PersonState {
    data: IPerson;
    list: IPerson[]
}

const initialState: PersonState = {
    data: new Person(),
    list: []
} 

export const personSlice = createSlice({
    name: 'person',
    initialState,
    reducers: { 
        setData: (state, action: PayloadAction<IPerson>) => {
            state.data = action.payload
        },
        setPersons: (state, action: PayloadAction<IPerson[]>) => {
            state.list = action.payload
        },
    }
})

export const { setData, setPersons } = personSlice.actions

export default personSlice.reducer

src\app\store.ts

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import personReducer from '../features/person/personSlice'

const customizedMiddleware = getDefaultMiddleware({
  serializableCheck: false
})

export default configureStore({
  reducer: {
    person: personReducer,
  },
  middleware: customizedMiddleware, 
})

5 – Components

src\app\App.ts

 
import { Form } from '../components/Form';
import { Table } from '../components/Table';  

function App() {
  
  return (
    <section className="bg-white ">
        <div className="container mt-8 px-6 py-12 mx-auto bg-transparent">

            <p  style={{fontSize:32, padding:0}}>React + Redux Toolkit + Typescript</p> 

            <hr className="my-8 border-gray-200 dark:border-gray-700" />
            
            <div className="grid gap-6 grid-cols-2">
                <Table />      
                <Form />            
            </div>

        </div>
    </section>
  );
}

export default App;

src\components\Form\index.tsx

import { IPerson, Person } from "../../interfaces/Person";
import { useDispatch, useSelector } from "react-redux";
import { PersonState, setData, setPersons } from '../../features/person/personSlice';
import { PersonService } from "../../services/person.service";
import Swal from "sweetalert2";
import { useState } from "react";

export const Form = () => {

    const { person } = useSelector((state:{ person: PersonState }) => state);
    
    const [ errorForm, setErrorForm ] = useState({
        name: false,
        addres: false,
        phone: false
    })

    const dispatch = useDispatch();

    const personService = new PersonService();
  
    const setFormValue = (event:React.ChangeEvent<HTMLInputElement>) => { 
        dispatch(setData({ ... person.data, [event.target.id]: event.target.value }))
	}

    const isValidForm = ( ) => {
   
        const error = { name: false, addres: false, phone: false }

        if(!person.data.name) error.name = true 
        if(!person.data.address) error.addres = true; 
        if(!person.data.phone) error.phone = true; 

        setErrorForm(error) 

        return error.name || error.addres || error.phone;
    }

    const fetchUpdate = async (event:React.FormEvent<HTMLFormElement>) => {
        try {
            event.preventDefault() 
            const data:IPerson = await personService.put(person.data)
            // add item
            const dataArray:IPerson[] =  [...person.list]  
            // search index 
            let index:number = dataArray.findIndex((item:IPerson)=>item.id === data.id )
            // replace item 
            dataArray.splice(index, 1, data); 
            //update item
            dispatch(setPersons(dataArray))
            // for clean form
            dispatch(setData(new Person()))
 
            Swal.fire({ 
                icon: 'success',
                title: 'The data has been updated' 
            })
        } catch (error) {
            console.log(error)
        }
    }

    const fetchCreate = async (event:React.FormEvent<HTMLFormElement>) => {
        try {
            event.preventDefault() 
            // valid fields 
            if(isValidForm()) return null;

            const data:IPerson = await personService.post(person.data)
            // for clean form
            dispatch(setData(new Person()))
            // add item
            const dataArray:IPerson[] = [ ... person.list ]
            dataArray.push(data)
            dispatch(setPersons(dataArray))

            Swal.fire({ 
                icon: 'success',
                title: 'The data has been saved' 
            })
        } catch (error) {
            console.log(error)
        }
    }

    const inputCSS = "block w-full px-5 py-2.5 mt-2 text-gray-700 placeholder-gray-400 bg-white border border-gray-200 rounded-lg focus:border-blue-400 focus:ring-blue-400 focus:outline-none focus:ring focus:ring-opacity-40 "
    const inputError ="border-red-400"
    
    return (
    <div className="px-8 py-4 pb-8 rounded-lg bg-gray-50">
 
        <form onSubmit={(e)=>person.data.id?fetchUpdate(e):fetchCreate(e)}>
            
            <div className="mt-4">
                <label className="mb-2  text-gray-800">Name</label>
                <input 
                    id="name"
                    type="text" 
                    placeholder="Artyom Developer"
                    value={person.data.name}
                    onChange={(e)=>setFormValue(e)}
                    className={errorForm.name?inputCSS+inputError:inputCSS } />
                    {errorForm.name && <p className="mt-1 text-m text-red-400">This is field is required</p>}  
            </div>

            <div className="mt-4">
                <label className="mb-2  text-gray-800">Address</label>
                <input 
                    id="address"
                    type="text" 
                    placeholder="California Cll 100"
                    value={person.data.address}
                    onChange={(e)=>setFormValue(e)}
                    className={errorForm.addres?inputCSS+inputError:inputCSS } />
                    {errorForm.addres && <p className="mt-1 text-m text-red-400">This is field is required</p>}  
            </div>

            <div className="mt-4">
                <label className="mb-2  text-gray-800">Phone</label>
                <input 
                    id="phone"
                    type="text" 
                    placeholder="88888888" 
                    value={person.data.phone}
                    onChange={(e)=>setFormValue(e)}
                    className={errorForm.phone?inputCSS+inputError:inputCSS } />
                    {errorForm.phone && <p className="mt-1 text-m text-red-400">This is field is required</p>}  
            </div>

            <button className="w-full mt-8 bg-teal-600 text-gray-50 font-semibold py-2 px-4 rounded-lg">
                {person.data.id?"Save":"Create"}
            </button>
        </form>
    </div>
    )
}

src\components\Table\index.tsx

import { useDispatch, useSelector } from "react-redux"
import { IPerson, Person } from "../../interfaces/Person"
import { PersonState, setData, setPersons } from "../../features/person/personSlice";
import { useEffect } from "react";
import { PersonService } from "../../services/person.service";
import Swal from "sweetalert2";

export const Table = () => {
 
    const { person } = useSelector((state:{ person: PersonState }) => state);

    const personService = new PersonService();
    
    const dispatch = useDispatch();

    const fetchData  = async () => {
        try {
            const res:IPerson[] = await personService.getAll()
            dispatch(setPersons(res))
        } catch (error) {
            console.log('Error to failed load ==>',error)
        }
    }
    
    useEffect(()=>{ 
        fetchData() 
    },[ ]) 

    const onClickDelete = (item:IPerson) => {

        Swal.fire({
            title: 'Are you sure you want to delete?',
            showCancelButton: true,
            confirmButtonText: 'Confirm',
          }).then((result) => {
            /* Read more about isConfirmed, isDenied below */
            if (result.isConfirmed) {
                fetchDelete(item)
            } 
          })
          
    }

    const fetchDelete = async (item:IPerson) => {
        try {
            await personService.delete(item)
             
            Swal.fire({ 
                icon: 'success',
                title: 'the item has been deleted',
                showConfirmButton: false 
            })

            fetchData()

        } catch (error) {
            console.log('Error to failed load ==>',error)
        }
    }

    const onClickInfo = async (item:IPerson) => {

        try { 
            
            const data:IPerson = await personService.getById( item.id! )
             
            Swal.fire({
                title: 'Details',
                icon: 'info',
                html:
                  `<b>Name</b> : ${data.name} <br>` +
                  `<b>Address</b> : ${data.address} <br>` +
                  `<b>Phone</b> : ${data.phone} <br>`,
                showCloseButton: false,
                showCancelButton: false, 
                confirmButtonText: 'Ok' 
            })

        } catch (error) {
            console.log('Error ==>',error)
        } 
    }

    return (
        <div className="inline-block">

                <button className="bg-teal-600 text-gray-50 font-semibold py-2 px-4 rounded-lg"  onClick={()=>dispatch(setData(new Person()))}>
                    New
                </button>
                

                <div className="overflow-hidden border border-gray-200 md:rounded-lg">


                    <table className="min-w-full divide-y divide-gray-200">
                        <thead className="bg-slate-800">
                            <tr>
                                <th scope="col" className="px-12 py-3.5 text-slate-50 font-medium text-left">
                                    Name
                                </th>

                                <th scope="col" className="px-4 py-3.5 text-slate-50 font-medium text-left">
                                    Address
                                </th>

                                <th scope="col" className="px-4 py-3.5 text-slate-50 font-medium text-left">
                                    Phone
                                </th>

                                <th scope="col" className="px-4 py-3.5 text-slate-50 font-medium text-left">
                                     Actions
                                </th> 
                            </tr>
                        </thead>
                        <tbody className="bg-white divide-y divide-gray-200">
                            {
                                person.list.map((item:IPerson, i)=>{
                                    return(
                                    <tr key={i}>
                                        <td className="px-12 py-4 whitespace-nowrap">
                                            {item.name}
                                        </td>
                                        <td className="px-4 py-4 whitespace-nowrap">{item.address}</td> 
                                        <td className="px-4 py-4 whitespace-nowrap">{item.phone}</td>
                                        <td className="px-4 py-4 whitespace-nowrap">
                                            <div className="flex items-center gap-x-6">
 
                                                <button className="bg-sky-600 text-sky-50 font-semibold py-2 px-4 rounded-lg"  onClick={()=>onClickInfo(item)}>
                                                    Info
                                                </button>

                                                <button className="bg-gray-600 text-gray-50 font-semibold py-2 px-4 rounded-lg"  onClick={()=>dispatch(setData(item))}>
                                                    Edit
                                                </button>

                                                <button className="bg-red-600 text-gray-50 font-semibold py-2 px-4 rounded-lg" onClick={()=>onClickDelete(item)}>
                                                    Delete
                                                </button>
                                    
                                            </div>
                                        </td>
                                    </tr>
                                    )
                                })
                            }
                            
                        </tbody>
                    </table>
                </div> 
        </div>  
    )
}

src\index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux' 
import store from "./app/store";   
import './index.css';
import App from './app/App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Provider store={store}>   
      <App />
    </Provider> 
  </React.StrictMode>
); 

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Backend

1 – Crear entorno virtual

python -m venv venv
Activar
#windows 
venv\Scripts\activate.bat
#linux or mac
source venv/bin/activate

2 – Instalar Django

pip install django djangorestframework

3 – Iniciar proyecto

django-admin startproject tutofox
cd tutofox

4 – Instalar PostgreSQL

pip install psycopg2

Configurar conexión

tutofox\settings.py

...

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'tutofox',
        'USER': 'postgres',
        'PASSWORD': '12345',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

...

5 – Migrate

python manage.py migrate

6 – Crear app

django-admin startapp person

Configurar app

tutofox\settings.py

...

INSTALLED_APPS = [
 	...
    'rest_framework',
    'person'
]

...

7 – Model

person\models.py

from django.db import models
 
class PersonModel(models.Model):
    name = models.CharField(max_length=255)
    address = models.TextField()
    phone = models.BigIntegerField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "person"
        ordering = ['-created_at']

8 – Serialize

person\serializers.py

from rest_framework.serializers import ModelSerializer
from person.models import PersonModel


class PersonSerializer(ModelSerializer):
    class Meta:
        model = PersonModel
        fields = ['id', 'name', 'address', 'phone']
        #fields = '__all__'

9 – Views

person\views.py

from rest_framework import status
from rest_framework.views import APIView 
from rest_framework.response import Response
from person.models import PersonModel
from person.serializers import PersonSerializer 

class PersonApiView(APIView):
    def get(self, request):
        serializer = PersonSerializer(PersonModel.objects.all(), many=True)
        return Response(status=status.HTTP_200_OK, data=serializer.data)
    def post(self, request): 
        #res = request.data.get('name')  
        serializer = PersonSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(status=status.HTTP_200_OK, data=serializer.data)

class PersonApiViewDetail(APIView):
    def get_object(self, pk):
        try:
            return PersonModel.objects.get(pk=pk)
        except PersonModel.DoesNotExist:
            return None
    def get(self, request, id):
        post = self.get_object(id)
        serializer = PersonSerializer(post)  
        return Response(status=status.HTTP_200_OK, data=serializer.data)
    def put(self, request, id):
        post = self.get_object(id)
        if(post==None):
            return Response(status=status.HTTP_200_OK, data={ 'error': 'Not found data'})
        serializer = PersonSerializer(post, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(status=status.HTTP_200_OK, data=serializer.data) 
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    def delete(self, request, id):
        post = self.get_object(id)
        post.delete()
        response = { 'deleted': True }
        return Response(status=status.HTTP_204_NO_CONTENT, data=response)

9 – Urls

person\urls.py

from django.urls import path
from person.views import PersonApiView, PersonApiViewDetail
  
urlpatterns_persons = [
    path('v1/persons', PersonApiView.as_view()), 
    path('v1/persons/<int:id>', PersonApiViewDetail.as_view()), 
]

tutofox\urls.py

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include(urlpatterns_persons)),
]

10 – CORS

Install

pip install django-cors-headers 

tutofox\settings.py

INSTALLED_APPS = [
   ...
   'corsheaders'
]

MIDDLEWARE = [
	...
    'corsheaders.middleware.CorsMiddleware',
]

CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = (
  'http://localhost:3000',
)

11 – Start API Server

python manage.py makemigrations
python manage.py migrate
python manage.py runserver

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *