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