Tutorial Fullstack: React Typescript + Golang Gin + PostgreSQL

En este tutorial, le mostraré cómo crear un proyecto fullstack de React + Go + Gin+ PostgreSQL con una aplicación CRUD. El servidor back-end usaremos Go + Gin 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 + Go + Gin+ PostgreSQL project with a CRUD application. The back-end server we will use Go + Gin 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(id:number,data:IPerson) {
        try {
            const response = await api.put<IPerson>(`${this.apiURL}/${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 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 personService = new PersonService();
  
    const setFormValue = (event:React.ChangeEvent<HTMLInputElement>) => { 
        dispatch(setData({ ... person.data, [event.target.id]: event.target.value }))
	}

    const fetchUpdate = async (event:React.FormEvent<HTMLFormElement>) => {
        try {
            event.preventDefault() 
            //const data:IPerson = await personService.put(person.data)
            const data:IPerson = await personService.put(person.data.id!,{ 
                name: person.data.name,
                address: person.data.address,
                phone: parseInt(person.data.phone.toString())
            })
            // 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)
            const data:IPerson = await personService.post({ 
                name: person.data.name,
                address: person.data.address,
                phone: parseInt(person.data.phone.toString())
            })
            // 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 and Install

#create folder 
mkdir go-backed & cd go-backend
# init project
go mod init tutofox.com/go-backend
# install framework
go get -u github.com/gin-gonic/gin
# install ORM
go get -u gorm.io/gorm
# install driver database
go get -u gorm.io/driver/postgres
# cors 
got get -u github.com/itsjamie/gin-cors

2 – Database

configs\database.go

package configs

import (
	"log"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

var DB *gorm.DB

func ConnectToDB() {
	var err error
	dsn := "host=localhost user=postgres password=12345 dbname=company port=5432 sslmode=disable"
	DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})

	if err != nil {
		log.Fatal("Failed to connect DB")
	}
}

3 – Model

models\PersonModel.go

package models

import (
	"time"
)

type Person struct {
	ID        int32     `gorm:"primaryKey;column:id" json:"id"`
	Name      string    `gorm:"column:name" json:"name"`
	Address   string    `gorm:"column:address" json:"address"`
	Phone     uint      `gorm:"column:phone" json:"phone"`
	CreatedAt time.Time `gorm:"column:createdAt;autoCreateTime" json:"createdAt"`
	UpdatedAt time.Time `gorm:"column:updatedAt;autoUpdateTime:milli" json:"updatedAt"`
}

func (Person) TableName() string {
	return "persons"
}

4 – Migrate

migrate\migrate.go

import (
	"tutofox.com/go-backend/configs"
	"tutofox.com/go-backend/models"
)

func init() {
	configs.ConnectToDB()
}

func main() {
	configs.DB.AutoMigrate(&models.Person{})
}


go run migrate/migrate.go

4 – Controllers

controllers\PersonController.go

 package controllers

import (
	"github.com/gin-gonic/gin"
	"tutofox.com/go-backend/configs"
	"tutofox.com/go-backend/models"
)

type PersonRequestBody struct {
	Id      string `json:"id"`
	Name    string `json:"name"`
	Address string `json:"address"`
	Phone   uint   `json:"phone"`
}

func PersonCreate(c *gin.Context) {

	body := PersonRequestBody{}

	c.BindJSON(&body)

	person := &models.Person{Name: body.Name, Address: body.Address, Phone: body.Phone}

	result := configs.DB.Create(&person)

	if result.Error != nil {
		c.JSON(500, gin.H{"Error": "Failed to insert"})
		return
	}

	c.JSON(200, &person)
}

func PersonGet(c *gin.Context) {
	var persons []models.Person
	configs.DB.Find(&persons)
	c.JSON(200, &persons)
	return
}

func PersonGetById(c *gin.Context) {
	id := c.Param("id")
	var person models.Person
	configs.DB.First(&person, id)
	c.JSON(200, &person)
	return
}

func PersonUpdate(c *gin.Context) {

	id := c.Param("id")
	var person models.Person
	configs.DB.First(&person, id)

	body := PersonRequestBody{}
	c.BindJSON(&body)
	data := &models.Person{Name: body.Name, Address: body.Address, Phone: body.Phone}

	result := configs.DB.Model(&person).Updates(data)

	if result.Error != nil {
		c.JSON(500, gin.H{"Error": true, "message": "Failed to update"})
		return
	}

	c.JSON(200, &person)
}

func PersonDelete(c *gin.Context) {
	id := c.Param("id")
	var person models.Person
	configs.DB.Delete(&person, id)
	c.JSON(200, gin.H{"deleted": true})
	return
}

4 – Routes

routes\PersonRoutes.go

import (
	"github.com/gin-gonic/gin"
	"tutofox.com/go-backend/controllers"
)

func PersonRouter(router *gin.Engine) {

	routes := router.Group("api/v1/persons")
	routes.POST("", controllers.PersonCreate)
	routes.GET("", controllers.PersonGet)
	routes.GET("/:id", controllers.PersonGetById)
	routes.PUT("/:id", controllers.PersonUpdate)
	routes.DELETE("/:id", controllers.PersonDelete)

}

5 – Server

main.go

package main

import (
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	cors "github.com/itsjamie/gin-cors"
	"tutofox.com/go-backend/configs"
	"tutofox.com/go-backend/routes"
)

func init() {
	configs.ConnectToDB()
}

func main() {

	r := gin.Default()

	// Apply the middleware to the router (works with groups too)
	r.Use(cors.Middleware(cors.Config{
		Origins:         "*",
		Methods:         "GET, PUT, POST, DELETE",
		RequestHeaders:  "Origin, Authorization, Content-Type",
		ExposedHeaders:  "",
		MaxAge:          50 * time.Second,
		Credentials:     false,
		ValidateHeaders: false,
	}))

	routes.PersonRouter(r)

	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "Hello world from server Go.",
		})
	})
	r.Run()
}
go run  main.go

Deja una respuesta

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