Building a Full-Stack Todo App with Esbuild, React & Golang

In this post, we explore how to build a full-stack to-do application using Go, React, Esbuild, and import maps—eliminating the need for traditional Node modules. Our setup enables an efficient workflow with server-side rendering, client-side routing, and seamless state management. Backend: Go HTTP Server and Page Rendering Our backend is a simple HTTP server built with Go. It serves static files, handles API requests, and dynamically renders pages using Esbuild. Initializing the Server The main.go file defines our HTTP server. It sets up a SQLite database for managing to-do items and builds necessary frontend files: func init() { stdLibFiles, _ := filepath.Glob("ui/lib/*.jsx") api.Build(api.BuildOptions{ EntryPoints: stdLibFiles, Bundle: true, Write: true, Outdir: ".web/lib", JSX: api.JSXAutomatic, External: []string{"react", "react-dom", "@mui/material", "@emotion/react", "@emotion/styled"}, Format: api.FormatESModule, MinifyWhitespace: true, }) controller = *controllers.New(db.OpenConnection()) if _, err := controller.DB.Exec(` CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, description TEXT, is_completed BOOLEAN ) `); err != nil { panic(err) } } Serving Pages We define routes for the home and about pages, leveraging the RenderPage function to dynamically inject data: http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { defer controllers.HandleError(w) todos := controller.GetTodos() isCode := r.URL.Query().Get("isCode") == "true" content := controller.RenderPage("index", map[string]any{"todos": todos}, isCode) w.WriteHeader(http.StatusOK) w.Write(content) }) The RenderPage function compiles JSX files using Esbuild and injects context data: func (c *Controller) RenderPage(page string, ctx map[string]any, isCode bool) []byte { tmpl, err := template.New("__WEB_ROOT__").Funcs(template.FuncMap{ "marshal": marshal, }).Parse(HTML_PAGE) if err != nil { panic(err) } result := api.Build(api.BuildOptions{ EntryPoints: []string{fmt.Sprintf("ui/pages/%s.jsx", page)}, Bundle: true, Write: false, JSX: api.JSXAutomatic, External: []string{"react", "react-dom", "@lib", "@mui/material", "@emotion/react", "@emotion/styled"}, Format: api.FormatESModule, }) codeBytes := result.OutputFiles[0].Contents code := b64.StdEncoding.EncodeToString(codeBytes) code = fmt.Sprintf("data:text/javascript;base64,%s", code) if isCode { return []byte(code) } buf := new(bytes.Buffer) args := map[string]any{ "Page": page, "Context": ctx, "Code": code, } if err := tmpl.Execute(buf, args); err != nil { panic(err) } return buf.Bytes() } var HTML_PAGE = ` {{ if .Context }} {{ .Context | marshal }} {{ else }} {} {{ end }} { "imports": { "react": "https://esm.sh/react@18.2.0", "react/": "https://esm.sh/react@18.2.0/", "react-dom": "https://esm.sh/react-dom@18.2.0", "react-dom/": "https://esm.sh/react-dom@18.2.0/", "@lib/": "/_lib/", "@mui/material": "https://esm.sh/@mui/material?external=react,react-dom,@emotion/react,@emotion/styles&exports=Box,Typography,TextField,Button,List,ListItem,ListItemText", "@emotion/react": "https://esm.sh/@emotion/react?external=react,react-dom", "@emotion/styled": "https://esm.sh/@emotion/styled?external=react,react-dom" } } import ui from "{{ .Code }}"; import { createElement } from "react"; import { createRoot } from "react-dom/client"; globalThis.____WEB_ROOT = createRoot( document.getElementById("root"), ); globalThis.____WEB_ROOT.render(createElement(ui)); window.addEventListener("popstate", async (e) => { const code = await ( await fetch(`${e.target.location.pathname}?isCode=true`) ).text(); const m = await import(code); globalThis.____WEB_ROOT.render(createElement(ui)); }); ` Frontend: Import Maps, Client-Side Routing, and Context Management Our frontend architecture follows a modular approach, using import maps to manage dependencies ins

Feb 15, 2025 - 21:05
 0
Building a Full-Stack Todo App with Esbuild, React & Golang

In this post, we explore how to build a full-stack to-do application using Go, React, Esbuild, and import maps—eliminating the need for traditional Node modules. Our setup enables an efficient workflow with server-side rendering, client-side routing, and seamless state management.

Backend: Go HTTP Server and Page Rendering

Our backend is a simple HTTP server built with Go. It serves static files, handles API requests, and dynamically renders pages using Esbuild.

Initializing the Server

The main.go file defines our HTTP server. It sets up a SQLite database for managing to-do items and builds necessary frontend files:

func init() {
    stdLibFiles, _ := filepath.Glob("ui/lib/*.jsx")
    api.Build(api.BuildOptions{
        EntryPoints:      stdLibFiles,
        Bundle:           true,
        Write:            true,
        Outdir:           ".web/lib",
        JSX:              api.JSXAutomatic,
        External:         []string{"react", "react-dom", "@mui/material", "@emotion/react", "@emotion/styled"},
        Format:           api.FormatESModule,
        MinifyWhitespace: true,
    })

    controller = *controllers.New(db.OpenConnection())

    if _, err := controller.DB.Exec(`
        CREATE TABLE IF NOT EXISTS todos (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT,
            description TEXT,
            is_completed BOOLEAN
        )
    `); err != nil {
        panic(err)
    }
}

Serving Pages

We define routes for the home and about pages, leveraging the RenderPage function to dynamically inject data:

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    defer controllers.HandleError(w)

    todos := controller.GetTodos()
    isCode := r.URL.Query().Get("isCode") == "true"
    content := controller.RenderPage("index", map[string]any{"todos": todos}, isCode)

    w.WriteHeader(http.StatusOK)
    w.Write(content)
})

The RenderPage function compiles JSX files using Esbuild and injects context data:

func (c *Controller) RenderPage(page string, ctx map[string]any, isCode bool) []byte {
    tmpl, err := template.New("__WEB_ROOT__").Funcs(template.FuncMap{
        "marshal": marshal,
    }).Parse(HTML_PAGE)

    if err != nil {
        panic(err)
    }

    result := api.Build(api.BuildOptions{
        EntryPoints: []string{fmt.Sprintf("ui/pages/%s.jsx", page)},
        Bundle:      true,
        Write:       false,
        JSX:         api.JSXAutomatic,
        External:    []string{"react", "react-dom", "@lib", "@mui/material", "@emotion/react", "@emotion/styled"},
        Format:      api.FormatESModule,
    })

    codeBytes := result.OutputFiles[0].Contents
    code := b64.StdEncoding.EncodeToString(codeBytes)
    code = fmt.Sprintf("data:text/javascript;base64,%s", code)

    if isCode {
        return []byte(code)
    }

    buf := new(bytes.Buffer)
    args := map[string]any{
        "Page":    page,
        "Context": ctx,
        "Code":    code,
    }

    if err := tmpl.Execute(buf, args); err != nil {
        panic(err)
    }

    return buf.Bytes()
}

var HTML_PAGE = `
    
        
        
        
    
    
        
`

Frontend: Import Maps, Client-Side Routing, and Context Management

Our frontend architecture follows a modular approach, using import maps to manage dependencies instead of Node modules.

Client-Side Navigation (link.js)

The Link component provides seamless navigation without full-page reloads:

import { createElement } from "react";

export const Link = ({ to, children }) => {
  return (
    <div
      onClick={async () => {
        const path = `${to}?isCode=true`;
        const code = await fetch(path);
        const m = await import(await code.text());

        history.pushState({}, "", to);
        const root = globalThis.____WEB_ROOT;
        root.render(createElement(m.default));
      }}
    >
      {children}
    div>
  );
};

Managing Global State (context.js)

The context.js file extracts and provides server-injected data:

export const usePageContext = () => {
  return JSON.parse(document.getElementById("__WEB_ROOT_DATA__").text);
};

Home Page (index.jsx)

The home page displays the list of to-do items retrieved from the server:

import { useState } from "react";
import { Link } from "@lib/link.js";
import { usePageContext } from "@lib/context.js";

import {
  Button,
  Typography,
  Box,
  TextField,
  List,
  ListItem,
  ListItemText,
} from "@mui/material";

const HomePage = () => {
  const [count, setCount] = useState(0);

  const data = usePageContext();

  return (
    <Box>
      <Box
        borderBottom={2}
        borderColor={(theme) => theme.palette.divider}
        p={1}
        display={"flex"}
      >
        <Box flex={1}>
          <Typography>DashboardTypography>
        Box>
        <Link to="/about">
          <Button>About PageButton>
        Link>
      Box>
      <Box p={4}>
        <Typography>Todo ListTypography>

        <form method="POST" action={"/createTodo"}>
          <Box display="flex" flexDirection="column" gap={2} p={2}>
            <TextField
              type="text"
              required={true}
              name="title"
              placeholder="Title"
              variant="outlined"
            />
            <TextField
              type="text"
              required={true}
              name="description"
              placeholder="Description"
              variant="outlined"
            />
            <Box>
              <Button type="submit" variant="contained">
                Create
              Button>
            Box>
          Box>
        form>
        <Box mt={2}>
          <List>
            {data.todos.map((todo) => (
              <ListItem key={todo.id}>
                <ListItemText
                  primary={todo.title}
                  secondary={todo.description}
                />
              ListItem>
            ))}
          List>
        Box>
      Box>
    Box>
  );
};

export default HomePage;

About Page (about.jsx)

A simple about page:

import { Typography, Box, Button } from "@mui/material";
import { Link } from "@lib/link.js";

const AboutPage = () => {
  return (
    <Box>
      <Typography>About PageTypography>
      <Link to="/">
        <Button>HomeButton>
      Link>
    Box>
  );
};

export default AboutPage;

Conclusion

This article demonstrates how we can leverage import maps and esbuild to create a React-based full-stack application without relying on Node modules. By dynamically injecting server-side data into the frontend, we achieve a lightweight and efficient architecture. The approach outlined ensures seamless bundling, efficient routing, and a structured way to manage UI components while keeping dependencies minimal.

For the full code example, visit:
https://github.com/rohit20001221/go-esbuild-react-todo.git