go-route-gen
A tool I built to get type-safe routes between my Go backends and TypeScript frontends.
I built this tool during my internship to save myself from the repetitive task of syncing my backend routes with my frontend. I was tired of manually writing endpoint strings and making typos in path parameters. It’s not fun to have a bug just because you wrote {userid} instead of {user_id}.
The Lightbulb Moment: AST vs Regex
I originally tried to solve this with simple regex to scrape my Go files for route definitions. It worked for about ten minutes, until my Go code got more complex (like nested handlers or custom formatting) and the regex completely fell apart.
That’s when I decided to learn about Abstract Syntax Trees (AST). Instead of treating my code like a big string, I used Go's go/ast and go/parser packages to actually parse the language. It was a huge learning curve, but it made the scraper 100% more reliable.
func scrapeRoutes(dir string) (map[string]string, error) {
routes := make(map[string]string)
fset := token.NewFileSet()
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") {
return nil
}
node, err := parser.ParseFile(fset, path, nil, 0)
if err != nil {
return nil
}
ast.Inspect(node, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok || (sel.Sel.Name != "Handle" && sel.Sel.Name != "HandleFunc") {
return true
}
if len(call.Args) > 0 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok && lit.Kind == token.STRING {
rawRoute := strings.Trim(lit.Value, "\"")
parts := strings.Split(rawRoute, " ")
if len(parts) == 2 {
method := parts[0]
pathUrl := parts[1]
key := strings.ReplaceAll(pathUrl, "/", "_")
key = strings.ReplaceAll(key, "-", "_")
key = strings.ReplaceAll(key, "{", "")
key = strings.ReplaceAll(key, "}", "")
fullKey := fmt.Sprintf("%s_%s", method, strings.ToUpper(strings.Trim(key, "_")))
routes[fullKey] = rawRoute
}
}
}
return true
})
return nil
})
return routes, err
}
The result
✔ Generated 89 routes to route.ts
___ ____ __ ____________ _________ __
/ _ \/ __ \/ / / /_ __/ __/ / ___/ __/__/ /
/ , _/ /_/ / /_/ / / / / _/ / (_ / _// _ /
/_/|_|\____/\____/ /_/ /___/ \___/___/\_,_/
Target ➜ ./internal/api/handlers
Output ➜ ../apps/web/packages/shared/src/api/route.ts
Watching for changes...
Now, the tool scrapes my Go code, finds where I've defined net/http routes, and generates a strictly-typed TypeScript client. My frontend uses template literal types, so the compiler actually yells at me if I try to request a route that doesn't exist.
It’s not perfect—I still want to add support for response body types so the whole thing is truly end-to-end typed—but for now, it's saved me hours of debugging "404 Not Found" errors.