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