Tutorial Fullstack: React Typescript + NodeJS Express + PostgreSQL

En este tutorial, le mostraré cómo crear un proyecto fullstack de React + Node.js + Express + PostgreSQL con una aplicación CRUD. El servidor back-end usaremos Node.js + Express para API REST, el front-end con una aplicacion 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 + Node.js + Express + PostgreSQL project with a CRUD application. The back-end server we will use Node.js + Express 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 – Create Server NodeJS

# create folder
mkdir node-backend
# access folder
cd node-backend
# init project node
npm init -y

Install all dependencies and library

# framework nodejs
npm install express --save
# ORM 
npm install sequelize --save
# Driver postgreSQL
npm install pg pg-hstore --save 
# Reload server
npm install nodemon --save
# enviroment 
npm install dotenv --save
# cors
npm install cors --save

src\app.js

const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');

dotenv.config();
const app = express(); 

const port = 8080;
app.use(cors());
app.use(express.json());
 
app.get('/', (req,res) => {
    res.send('Backend con NodeJS - Express + CRUD API REST + POSTGRESQL');
}); 
  
app.listen(port,()=>{
    console.log("Port ==> ", port);
});

package.json

  "scripts": {
    "dev": "nodemon src/app.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

2 – ORM Sequelize

2.1 Database

.env

PORT=3000
DB_USER='postgres'
DB_PASSWORD='12345'
DB_HOST='localhost'
DB_NAME='company'
DB_PORT='5432'

src\config\config.js

require('dotenv').config();

const config = {
  env: process.env.NODE_ENV || 'dev',
  port: process.env.PORT || 3000,
  dbUser:  process.env.DB_USER,
  dbPassword:  process.env.DB_PASSWORD,
  dbHost:  process.env.DB_HOST,
  dbName:  process.env.DB_NAME,
  dbPort:  process.env.DB_PORT,
}

module.exports = { config };

src\config\database.js

const { Sequelize } = require('sequelize');

const  { config } = require('./config');
const setupModels = require('../models');
  
const sequelize = new Sequelize(
    config.dbName, // name database
    config.dbUser, // user database
    config.dbPassword, // password database
    {
      host: config.dbHost,
      dialect: 'postgresql' 
    }
  );

sequelize.sync();
setupModels(sequelize);

module.exports = sequelize;

2.2 Model

src\models\index.js

const { Person, PersonSchema } = require('./person.model');

function setupModels(sequelize) {
    Person.init(PersonSchema, Person.config(sequelize));
}

module.exports = setupModels;

src\models\person.model.js

const { Model, DataTypes } = require('sequelize');

const PERSON_TABLE = 'persons';

class Person extends Model {
    static config(sequelize) {
        return {
            sequelize,
            tableName: PERSON_TABLE,
            modelName: 'Person',
            timestamps: true
        }
    }
} 

const PersonSchema = {
    id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: DataTypes.INTEGER
    },
    name: {
        allowNull: false,
        type: DataTypes.STRING,
        field:'name'
    },
    address:{ 
        allowNull:false,
        type: DataTypes.STRING,
        field: 'address'
    },
    phone:{
        allowNull: true,
        type: DataTypes.INTEGER,
        field: 'phone'
    } 
}
  
module.exports = { Person, PersonSchema };

3 – Services

src\services\person.service.js

const { models } = require('../config/database');

class PersonsService { 
  
    constructor() {}

    async find() {
      const res = await models.Person.findAll();
      return res;
    }

    async findOne(id) {
      const res = await models.Person.findByPk(id);
      return res;
    }

    async create(data) {
      const res = await models.Person.create(data);
      return res;
    }

    async update(id, data) {
      const model = await this.findOne(id);
      const res = await model.update(data);
      return res;
    }

    async delete(id) {
      const model = await this.findOne(id);
      await model.destroy();
      return { deleted: true };
    }
  
  }
  
  module.exports = PersonsService;

4 – Controllers

src\controllers\person.controller.js

const PersonsService = require('../services/person.service');
const service = new PersonsService();

const create = async ( req, res ) => {
    try { 
        const response = await service.create(req.body);
        res.json(response);
    } catch (error) {
        res.status(500).send({ success: false, message: error.message });
    }
}

const get = async ( req, res ) => {
    try {
        const response = await service.find();
        res.json(response);
    } catch (error) {
        res.status(500).send({ success: false, message: error.message });
    }
}

const getById = async ( req, res ) => {
    try {
        const { id } = req.params;
        const response = await service.findOne(id);
        res.json(response);
    } catch (error) {
        res.status(500).send({ success: false, message: error.message });
    }
}

const update = async (req, res) => {
    try {
        const { id } = req.params;
        const body = req.body;
        const response = await service.update(id,body);
        res.json(response);
    } catch (error) {
        res.status(500).send({ success: false, message: error.message });
    }
}

const _delete = async (req, res) => {
    try {
        const { id } = req.params; 
        const response = await service.delete(id);
        res.json(response);
    } catch (error) {
        res.status(500).send({ success: false, message: error.message });
    }
}

module.exports = {
    create, get, getById, update, _delete
};

4 – Routes

src\routes\index.js

const express = require('express'); 

const personsRouter = require('./person.router');

function routerApi(app) {
  const router = express.Router();
  app.use('/api/v1', router); 
  router.use('/persons', personsRouter);
}

module.exports = routerApi;

src\routes\person.router.js

const express = require('express');
const router = express.Router(); 
const personsController = require('../controllers/person.controller');

router
    .get('/', personsController.get )
    .get('/:id', personsController.getById )
    .post('/', personsController.create )
    .put('/:id', personsController.update )
    .delete('/:id', personsController._delete );

module.exports = router;

5 – App server

src\app.js

const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');

dotenv.config();
const app = express(); 

const port = 8080;
app.use(cors());
app.use(express.json());

const routerApi = require('./routes');


app.get('/', (req,res) => {
    res.send('Backend con NodeJS - Express + CRUD API REST + POSTGRESQL');
}); 

routerApi(app);

app.listen(port,()=>{
    console.log("Port ==> ", port);
});
npm run dev

Deja una respuesta

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