Tutorial Fullstack: React Typescript + Laravel 10 PHP 8+ PostgreSQL

react typescript laravel postgresql

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

composer create-project laravel/laravel backend-laravel

2 – Configure Database

Recuerde que debe habilitar la extensión pdo_pgsql en el archivo php.ini para poder conectar a la BD de PostgreSQL.
Remember that you must enable the pdo_pgsql extension in the php.ini file to be able to connect to the PostgreSQL DB

.env

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1 
DB_PORT=5432
DB_DATABASE=tutofox
DB_USERNAME=postgres
DB_PASSWORD=12345

3 – Migrate

php artisan make:migration create_person_table 

database/migrations/2021_05_17_011217_create_person_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePersonTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('person', function (Blueprint $table) {
            $table->id();
            $table->string('name',100);
            $table->string('address',200);
            $table->bigInteger('phone');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('person');
    }
}

Run migrate / ejecutar migrate

php artisan migrate

4 – Model

php artisan make:model Person

app/Models/Person.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Person extends Model
{
    use HasFactory;

    protected $table = "person";

    protected $fillable = [
      'name',
      'address',
      'phone'
    ];
 
}

5 – Controller

php artisan make:controller API/PersonController

app/Http/Controllers/API/PersonController.php

<?php

namespace App\Http\Controllers\API;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Person;

class PersonController extends Controller
{
    public function get(){
        try { 
            $data = Person::get();
            return response()->json($data, 200);
        } catch (\Throwable $th) {
            return response()->json([ 'error' => $th->getMessage()], 500);
        }
    }
  
    public function create(Request $request){
        try {
            $data['name'] = $request['name'];
            $data['address'] = $request['address'];
            $data['phone'] = $request['phone'];
            $res = Person::create($data);
            return response()->json( $res, 200);
        } catch (\Throwable $th) {
            return response()->json([ 'error' => $th->getMessage()], 500);
        }
    }

    public function getById($id){
        try { 
            $data = Person::find($id);
            return response()->json($data, 200);
        } catch (\Throwable $th) {
            return response()->json([ 'error' => $th->getMessage()], 500);
        }
    }

    public function update(Request $request,$id){
        try { 
            $data['name'] = $request['name'];
            $data['address'] = $request['address'];
            $data['phone'] = $request['phone'];
            Person::find($id)->update($data);
            $res = Person::find($id);
            return response()->json( $res , 200);
        } catch (\Throwable $th) {
            return response()->json([ 'error' => $th->getMessage()], 500);
        }
    }
  
    public function delete($id){
        try {       
            $res = Person::find($id)->delete(); 
            return response()->json([ "deleted" => $res ], 200);
        } catch (\Throwable $th) {
            return response()->json([ 'error' => $th->getMessage()], 500);
        }
    }
      
}

6 – Routes

routes/api.php

use App\Http\Controllers\API\PersonController;

Route::prefix('v1/persons')->group(function () {
    Route::get('/',[ PersonController::class, 'get']);
    Route::post('/',[ PersonController::class, 'create']);
    Route::delete('/{id}',[ PersonController::class, 'delete']);
    Route::get('/{id}',[ PersonController::class, 'getById']);
    Route::put('/{id}',[ PersonController::class, 'update']);
});

7 – Run server

php artisan serve

Deja una respuesta

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