A Small Javascript-to-Go Migration with Claude Code

I have a few scripts that I run daily, mostly focused around processing stats and recreating my dashboards (static HTMLs with a chart viewer). On their latest iteration, I built them in Javascript, and while they serve their purpose, that poses two limitations:

  1. I need to have Node.js installed in the server. Not a big deal, but these scripts are the only reason.
  2. Over time, the scripts needed increasingly aggressive preprocessing because some files contain years of stats. I ended up splitting cleaning and processing into separate incremental steps just to avoid excessive memory usage in Node.js.

Yesterday, I decided to try using AI to migrate the code to something more performant, like Go. I have good examples I can feed an AI agent for guidance. I can also point it to the real stats files to build some tests (which the javascript version didn't have). And I also have my set of prompts, skills, and subagents (including an initial version of a go-expert agent) to apply good design patterns.

The code I gave it as guidance is a more advanced version of https://github.com/Kartones/go-cli/, but the structure is almost the same, unit tests being the biggest addition (which the AI agent also replicated).

I've already mentioned why I think Go is great for tooling, so because of my cross-compilation needs and my preference for Gazelle to avoid managing build configuration files, it was the perfect choice.

I booted up Claude Code (Sonnet 4.6 200 KB context), and sent it this prompt:

At `<js-scripts-path>` you have some Javascript scripts that I'd like to port over to Golang, so that we can generate macOS and Linux binaries, avoid requiring Node.js. Analyze that folder (there are multiple scripts), and let's create a `scripts_go` new folder where to build the Go rewrite.

We must use Bazel for building the Go version, use `<reference-go-project-path>` for reference to copy the idea: Makefile with commands, Bazel usage, etc., and as I mentioned, both macOS and Linux targets.

This is an example (from an older version of kc) of how to do cross compilation in Bazel:
<markdown-paste-of-go-cli-cross-compilation-bazel-example>

It took around 10 minutes to generate an initial version, almost one-shotting it. There was a subtle bug due to a difference in Go vs JS of handling string templating (easy fix). I noticed that it did not migrate all the comments, so I forced it to do a file by file comparison of both versions where applicable. I also had to remove some cross-compilation combinations that I didn't want (darwin amd64 and linux arm64), but kudos for being so complete by default.

This is the summary that Claude generated inspecting its session history to extract how much it spent on this, including me reviewing the code:

Duration: ~20 minutes

The session covered porting JavaScript scripts in <js-scripts-path> to Go, and included a review pass to ensure comments and details weren't missed when comparing JS vs Go versions file by file.

The generated tests are reasonably representative, but don't cover all cases. Code coverage is not set up, so that's on me for not providing a metric for the agent to optimize for. Still, much better than in past times me manually running against the stat files and ensuring by hand things were ok.

I estimate that this would have taken me half-day of work to do it manually.

To wrap up, this is how the main BUILD.bazel file looks (and except for MODULE.bazel dependency updates, the only thing you need to manually write):

load("@gazelle//:def.bzl", "gazelle")
load("@rules_go//go:def.bzl", "go_cross_binary")

# gazelle:prefix github.com/kartones/daily-scripts
gazelle(
    name = "gazelle",
    tags = ["manual"],
)

go_cross_binary(
    name = "process-rss_darwin_arm64",
    platform = "@rules_go//go/toolchain:darwin_arm64",
    target = "//cmd/process-rss",
)

go_cross_binary(
    name = "process-rss_linux_amd64",
    platform = "@rules_go//go/toolchain:linux_amd64",
    target = "//cmd/process-rss",
)

# remaining tool binaries macOS/Linux pairs

To build it, I simply run make and it runs gazelle, builds all commands, grabs the binaries from Bazel output, and rsync them to my server. Simplified example:

.DEFAULT_GOAL := build-cross

# other actions, like build and test go here

.PHONY: build-cross
build-cross:
    rm -rf binaries
    mkdir -p binaries
    set -o pipefail ; \
    bazel run //:gazelle ; \
    bazel build \
      //:process-rss_darwin_arm64 \
      //:process-rss_linux_amd64
    @for name in process-rss_darwin_arm64 process-rss_linux_amd64; do \
      src=$$(bazel cquery --output=files //:$$name 2>/dev/null | grep "^bazel-out" | head -1); \
      if [ -n "$$src" ]; then cp "$$src" binaries/; echo "  $$name -> binaries/"; \
      else echo "  WARNING: $$name not found"; fi; \
    done
  # rsync/copy would go here

Summary

Getting a useful Javascript-to-Go rewrite in roughly 20 minutes with minimal effort, besides code review, is excellent. Memory footprint is dramatically smaller, I removed Node.js, and all scripts execute faster (not that this was a problem, but nice).

I suspect this worked particularly well because the scripts are deterministic and with clear inputs and outputs, plus I already had a Go + Bazel + Gazelle project structure the agent could imitate.

Tags: AI & ML Bazel Development Gazelle Go Javascript Patterns & Practices Testing

A Small Javascript-to-Go Migration with Claude Code article, written by Kartones. Publication date: