Fullstack web development con Go & ViteJS

Chi sono?

Antonio De Lucreziis, studente di Matematica e macchinista del PHC

PHC

Il PHC è un gruppo di studenti di Matematica con interessi per, open source, Linux, self-hosting e soprattutto smanettare sia con hardware e software (veniteci pure a trovare!)

Go e ViteJS

Go

ViteJS

Go, linguaggio di programmazione compilato, statically typed e con una libreria standard con tutte le cose essenziali.

Un tool per NodeJS per creare velocemente web app in HTML, JS, CSS utilizzando tutto l'ecosistema di NodeJS e NPM senza configurare quasi nulla

Perché Go e ViteJS?

Front-End

Back-End

  • Javascript

  • Typescript

  • WASM*
  • Python

  • NodeJS

  • Golang

  • Rust
  • ...

Cosa stiamo per creare?

Applicazione Contatore "Full Stack"

https://example-website.xyz/

Contatore: 73

Incrementa

Decrementa

Applicazione "Contatore"

  • Si va sul sito e poi si può premere uno di due pulsanti
  • Un tasto incrementa il valore mentre l'altro lo decrementa
  • Vedremo come anche un'applicazione semplice di questo tipo in realtà ci permette di trattare di molti concetti tra cui frontend, backend ed API (e se ce la facciamo anche Database)

Cosa succede quando proviamo ad andare su un sito?

Utente

Server

Connessione Client-Server

Utente

Server

http://example-website.xyz

Connessione Client-Server

\underbrace{\vphantom{htp/}\texttt{http}}_{\mathclap{\text{Protocollo}}} \texttt{://} \underbrace{\vphantom{htp/}\texttt{example.org}}_{\text{Dominio}} \, \texttt{:} \underbrace{\vphantom{htp/}\texttt{1234}}_{\mathclap{\text{Port}}} \, \underbrace{\vphantom{htp/}\texttt{/foo/bar}}_{\text{Path}} \, \underbrace{\vphantom{htp/}\texttt{?id=123}}_{\text{Query}}

URL?

Utente

Server

http://example-website.xyz

Connessione Client-Server

Utente

Server

http://123.123.123.123:80

Il DNS si occupa di risolvere il dominio e convertirlo in un IPv4 (o IPv6)

Connessione Client-Server

Ehm, e l'HTTPS?

Client

Server

Connessione TLS

Scambio di chiavi

Scambio di dati in modo sicuro (HTTP)

?

Server (Macchina)

:80

example.com (192.0.2.42)

Server (Macchina)

:80

Server (Programma)

example.com (192.0.2.42)

Creazione del progetto

$ mkdir gdg-counter-website
$ cd gdg-counter-website

Creazione progetto

# inizializziamo il file "go.mod"
$ go mod init gdg-talk-counter

# o se avessimo un repo git anche così...
$ go mod init github.com/aziis98/gdg-talk-counter

Inizializzazione progetto in Go

# Crea il file "package.json" 
$ npm init

# Installa ViteJS come dipendenza di development
$ npm install -D vite

Inizializzazione progetto di NodeJS

O anche con un altro package manager alternativo a npm (ad esempio io userò pnpm)

ViteJS (1)

{
    "name": "frontend",
    "version": "1.0.0",
    "scripts": {
    	// avvia il server di ViteJS in modalità di development
        "dev": "vite",
        // crea la cartella "dist/" con tutti gli asset e bundle
        "build": "vite build"
    },
    "author": "aziis98",
    "license": "MIT",
    "devDependencies": {
        "vite": "^3.2.3"
    }
}

Configurazione di base

package.json
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>GDG Talk Counter</title>
  </head>
  <body>
    <h1>GDG Counter Website</h1>
    <div class="app">
      <div id="counter-value">???</div>
      <button id="btn-increment">Incrementa</button>
      <button id="btn-decrement">Decrementa</button>  
    </div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

Pagina HTML

index.html
const counterElement = document.querySelector('#counter-value')
const incrementButton = document.querySelector('#btn-increment')
const decrementButton = document.querySelector('#btn-decrement')

function updateCounter(value) {
    counterElement.textContent = `Counter: ${value}`
}

Un po' di JS

src/main.js

Client

Server

PATCH esempio.org/a/b/c

Client

Server

POST esempio.org/a/b/c

Client

Server

GET esempio.org/a/b/c

Client

Server

DELETE esempio.org/a/b/c

Client

Server

PUT esempio.org/a/b/c

Client

Server

PATCH esempio.org/a/b/c

Client

Server

POST esempio.org/a/b/c

Client

Server

GET esempio.org/a/b/c

Client

Server

DELETE esempio.org/a/b/c

Client

Server

PUT esempio.org/a/b/c

Client

Server

GET /api/value

valore aggiornato del contatore

Client

Server

POST /api/increment

valore aggiornato del contatore

Client

Server

POST /api/decrement

valore aggiornato del contatore

/* ... */

incrementButton.addEventListener('click', () => {
    fetch('/api/increment', { method: 'POST' })
        .then(res => res.json())
        .then(data => updateCounter(data))
})

decrementButton.addEventListener('click', () => {
    fetch('/api/decrement', { method: 'POST' })
        .then(res => res.json())
        .then(data => updateCounter(data))
})

fetch('/api/value')
    .then(res => res.json())
    .then(data => updateCounter(data))

Colleghiamo i bottoni con il server

src/main.js
/* ... */

incrementButton.addEventListener('click', async () => {
    const res = await fetch('/api/increment', { method: 'POST' })
    const data = await res.json()
    updateCounter(data)
})

decrementButton.addEventListener('click', async () => {
    const res = await fetch('/api/decrement', { method: 'POST' })
    const data = await res.json()
    updateCounter(data)
})

const res = await fetch('/api/value')
const data = await res.json()
updateCounter(data)

Stessa cosa ma con async-await

src/main.js

Demo di quanto fatto fino ad ora

Go (1)

package main

import (
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()
    setupRoutes(mux) // visto meglio in seguito
    
    server := http.Server{
        Addr:    ":4000",
        Handler: mux,
    }

    log.Fatal(server.ListenAndServe())
}

Iniziamo a scrivere il server in Go

main.go
/* ... */

func setupRoutes(mux *http.ServeMux) {
    counter := 0

    mux.HandleFunc("/api/status", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        err := json.NewEncoder(w).Encode("ok")
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    })

    mux.Handle("/", http.FileServer((http.Dir("./dist/"))))
}

Serviamo gli asset principali

main.go

ViteJS (2)

import { defineConfig } from 'vite'

export default defineConfig({
    server: {
        port: 3000,
        proxy: {
            '/api': 'http://localhost:4000/',
        },
    },
})

Configurazione per ViteJS

vite.config.js

Proxy di ViteJS per il server in Go

Development

ViteJS Dev Server

Go Server

:3000 /api/...

:3000 /...

:4000 /api/...

{...}

{...}

:3000

Proxy di ViteJS per il server in Go

Production

Go Server

:4000 /...

:4000 /api/...

dist/...

{...}

:4000

Demo (2)

Go (2)

/* ... */

func setupRoutes(mux *http.ServeMux) {
    counter := 0

    /* ... */

    mux.HandleFunc("/api/value", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
            return
        }

        w.Header().Set("Content-Type", "application/json")
        err := json.NewEncoder(w).Encode(counter)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    })
    
    /* ... */
}

Route di API

main.go
counter := 0

/* ... */

mux.HandleFunc("/api/increment", func(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    counter++

    w.Header().Set("Content-Type", "application/json")
    err := json.NewEncoder(w).Encode(counter)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
})

Route di API

main.go
counter := 0

/* ... */

mux.HandleFunc("/api/decrement", func(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    counter--

    w.Header().Set("Content-Type", "application/json")
    err := json.NewEncoder(w).Encode(counter)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
})

Route di API

main.go

Demo (3)

Conclusione

Possibili Sviluppi

  • Database: SQLite (Per non resettare i dati ogni volta che il server viene spento)
  • Login e Utenti (Ogni utente ha un proprio counter su /u/USERNAME che gli altri possono cambiare)
  • Framework per la Frontend: Preact (Il nostro progetto in js vanilla era molto semplice, ma in generale sarebbe meglio usare un vero framework)
  • Dockerfile (per automatizzare il deployment)
  • WebSocket/SSE per vedere il counter aggiornarsi in diretta
  • SSR (Inviare al client HTML già con i valori giusti, al momento inizialmente il counter è "???" e senza JS non si può neanche vedere il valore corrente del counter)
  • . . .

Live code fino alla fine del tempo