Reuse Your Go Backend with a Next.js Frontend (Simple Styles, Same APIs)

July 29, 2025

In this sequel to Go + HTMX To-Do, we keep the exact same Go backend and layer a Next.js frontend on top. This is a great way to learn API-driven UIs without re-writing your server.

Goals

  • Reuse the Go server (no changes)
  • Next.js App Router frontend with simple styles
  • Client-side fetch for CRUD actions

1) Confirm Your Go Backend

Use the backend from the previous article. Ensure the following endpoints work:

  • GET / renders HTML (for HTMX) — we’ll ignore this on the Next.js side
  • POST /todos (form: title)
  • GET /toggle?id=1
  • GET /delete?id=1

Optional: add a JSON endpoint if you prefer fetching todos in JSON:

mux.HandleFunc("/api/todos", func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(store.todos)
})

2) Create Next.js Page

Create a new Next.js app (or add a route) that points to your Go server (running on http://localhost:8080).

// app/todos/page.tsx
'use client'
import { useEffect, useState } from 'react'

type Todo = { id: number; title: string; done: boolean }

const API = 'http://localhost:8080'

export default function TodosPage() {
  const [todos, setTodos] = useState<Todo[]>([])
  const [title, setTitle] = useState('')
  const [loading, setLoading] = useState(false)

  async function load() {
    setLoading(true)
    try {
      const res = await fetch(`${API}/api/todos`, { cache: 'no-store' })
      const data = await res.json()
      setTodos(data)
    } finally {
      setLoading(false)
    }
  }

  useEffect(() => { load() }, [])

  async function addTodo(e: React.FormEvent) {
    e.preventDefault()
    const form = new FormData()
    form.set('title', title)
    await fetch(`${API}/todos`, { method: 'POST', body: form })
    setTitle('')
    await load()
  }

  async function toggle(id: number) {
    await fetch(`${API}/toggle?id=${id}`)
    await load()
  }

  async function del(id: number) {
    await fetch(`${API}/delete?id=${id}`)
    await load()
  }

  return (
    <div className="mx-auto max-w-2xl p-6">
      <h1 className="text-2xl font-semibold mb-4">Next.js + Go To-Do</h1>

      <form onSubmit={addTodo} className="flex gap-2 mb-4">
        <input
          value={title}
          onChange={(e)=> setTitle(e.target.value)}
          placeholder="Add a task"
          className="flex-1 border rounded px-3 py-2"
        />
        <button className="border rounded px-3 py-2">Add</button>
      </form>

      {loading ? (
        <p>Loading…</p>
      ) : (
        <ul>
          {todos.length === 0 ? (
            <li>No todos yet  add one!</li>
          ) : (
            todos.map((t) => (
              <li key={t.id} className="flex items-center justify-between py-1">
                <label className="flex items-center gap-2">
                  <input
                    type="checkbox"
                    checked={t.done}
                    onChange={()=> toggle(t.id)}
                  />
                  <span className={t.done ? 'line-through text-gray-500' : ''}>{t.title}</span>
                </label>
                <button onClick={()=> del(t.id)} className="text-red-600"></button>
              </li>
            ))
          )}
        </ul>
      )}

      <p className="mt-6 text-sm text-gray-500">
        Backend: <code>{API}</code>
      </p>
    </div>
  )
}

If you don’t want to add a JSON endpoint, you can still scrape the server-rendered HTML or refactor the Go handlers to support both HTML and JSON via the Accept header.

3) SEO & DX Notes

  • Keep /todos crawlable by rendering a server component with initial data (optional).
  • Use your site’s dynamic OG image: /og?title=Next.js%20%2B%20Go%20To-Do&subtitle=Same%20Backend%2C%20New%20UI.
  • Add a canonical link if you cross-post tutorials.

4) Deploy

  • Deploy the Go server (Fly.io, Render, Railway, Cloud Run)
  • Deploy Next.js (Vercel)
  • Configure CORS or proxy via Next.js rewrites if needed

Conclusion: you can keep a clean, simple Go backend and still enjoy a polished React UI when you need it. For the starter server, see the article above; for a production-ready sample (auth, DB, migrations), ping me and I’ll publish one next.