DEV Community

aktky
aktky

Posted on

Tired of GUI builders? I made a code-first alternative to Retool

Hey DEV community! πŸ‘‹

Have you ever found yourself spending hours dragging and dropping components in a GUI builder when all you wanted was to quickly create an admin panel? That was me, and I got tired of it. So I built something different.

The drag-and-drop frustration

As a backend developer, I love working with databases, implementing business logic, and building APIs. But every time I needed to create an internal tool or admin panel, I'd struggle with GUI-based builders like Retool.

Don't get me wrong - Retool is powerful. But I kept running into issues:

  • πŸ€¦β€β™‚οΈ Moving components in the GUI would mysteriously break connections
  • 😡 Reviewing changes in pull requests was nearly impossible
  • πŸ€” Teaching new team members the proprietary UI was time-consuming
  • πŸ€– AI coding assistants couldn't help with GUI-based configurations

Plus, there was always this disconnect between my backend code and the frontend. I'd write a function in Go or TypeScript, then manually wire it up to a UI component through a confusing interface.

I just wanted to connect my backend functions directly to a UI!

The code-first dream

What if we could do this instead:

func usersPage(ui sourcetool.UIBuilder) error {
    // Simple UI definition in Go
    ui.Markdown("## Users")

    // Input directly connected to your function
    name := ui.TextInput("Name", textinput.WithPlaceholder("filter by name"))

    // Call your existing backend function
    users, err := listUsers(name)
    if err != nil {
        return err
    }

    // Display the results
    ui.Table(users)
    return nil
}
Enter fullscreen mode Exit fullscreen mode

That's it! No frontend code, no separate repository, no complex build setup. Just your backend code with some UI definitions.

This is exactly what Sourcetool does.

How it actually works

Sourcetool has a simple architecture with three main players:

  1. Your backend server (where your Go code runs)
  2. A Sourcetool server (handles auth and acts as a WebSocket bridge)
  3. The user's browser (where the UI renders)

When a user interacts with the UI (clicks a button, types in a field), that event travels through WebSockets to your backend, where your code handles it. The UI then updates in real-time based on your response.

It takes just a few lines to set up:

func main() {
    st := sourcetool.New(&sourcetool.Config{
        APIKey:   "YOUR_API_KEY",
        Endpoint: "wss://your-sourcetool-instance",
    })

    // Register your pages
    st.Page("/users", "Users", usersPage)
    st.Page("/products", "Products", productsPage)

    // Start the server
    if err := st.Listen(); err != nil {
        log.Fatal(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Perfect for the AI coding era

With tools like GitHub Copilot, ChatGPT, and Cursor changing how we write code, GUI builders feel increasingly outdated.

I can ask an AI to:

  • "Add a form with name and email fields that calls the createUser function"
  • "Add validation to this form"
  • "Create a new page for displaying analytics"

And since it's all just code, the AI can help directly - no screenshots or complex instructions needed!

The advantages over GUI builders

Here's why a code-first approach works better for me:

  1. Git-friendly - Review PRs, track changes, and understand the history of your app
  2. Type-safe - Catch errors at compile time, not when users are trying to use your app
  3. Familiar tools - Use your existing IDE, linters, and code review workflows
  4. AI-friendly - Let AI assistants help you build and modify your tools
  5. No context switching - Stay in your backend language rather than jumping between environments

Current status

Right now, Sourcetool supports Go with TypeScript and Python SDKs coming soon. It's entirely open-source under the Apache 2.0 license, so you can self-host and use it for free.

Here's a quick example of creating a form:

func createUserPage(ui sourcetool.UIBuilder) error {
    formUI, submitted := ui.Form("Create", form.WithClearOnSubmit(true))

    name := formUI.TextInput("Name", textinput.WithRequired(true))
    email := formUI.TextInput("Email", textinput.WithRequired(true))

    if submitted {
        _ = createUser(name, email)
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Join the community!

I've open-sourced Sourcetool on GitHub and would love your feedback. It's still early days, but if you share my frustrations with GUI builders, give it a try!

If you find this interesting, a GitHub star ⭐ would really help motivate further development! You can also reach out via Twitter/X DMs.

What do you think?

I'd love to hear your thoughts! Do you prefer GUI builders or code-first approaches? Have you experienced similar frustrations? Let me know in the comments below!


P.S. If you're curious about how we built the backend-to-frontend bridge, check out this deep dive into WebSocket architecture.

Top comments (0)