Step-by-step guide to implementing Go modules for JavaScript
Native modules bridge Go's capabilities with JavaScript's accessibility by implementing the NativeModule interface. Each module becomes a Node.js-style package that JavaScript can import via require(), enabling you to expose Go functionality while maintaining familiar JavaScript patterns.
Every native module follows a consistent pattern that includes interface implementation, automatic registration, and bidirectional type conversion. The module system handles the loading mechanism, while your code focuses purely on the exported functionality.
// modules/example/example.go
package examplemod
import (
"github.com/dop251/goja"
"github.com/go-go-golems/go-go-goja/modules"
)
type m struct{}
// Compile-time interface check
var _ modules.NativeModule = (*m)(nil)
func (m) Name() string {
return "example" // This becomes the require() name
}
func (m) Loader(vm *goja.Runtime, moduleObj *goja.Object) {
exports := moduleObj.Get("exports").(*goja.Object)
// Export functions to JavaScript
exports.Set("hello", func(name string) string {
return "Hello, " + name + "!"
})
}
func init() {
modules.Register(&m{}) // Auto-register during import
}
The goja runtime automatically converts between Go and JavaScript types according to these mappings:
Go to JavaScript:
string → Stringint, int64, float64 → Numberbool → Booleanmap[string]interface{} → Object[]interface{} → Arraynil → nullJavaScript to Go:
stringint, int64, float64 (as appropriate)boolmap[string]interface{}[]interface{}nilThese comprehensive examples demonstrate patterns beyond the basic template, showing how to handle complex data types, error conditions, and multiple function exports within a single module.
// modules/fs/fs.go
package fsmod
import (
"os"
"path/filepath"
"github.com/dop251/goja"
"github.com/go-go-golems/go-go-goja/modules"
)
type m struct{}
var _ modules.NativeModule = (*m)(nil)
func (m) Name() string { return "fs" }
func (m) Loader(vm *goja.Runtime, moduleObj *goja.Object) {
exports := moduleObj.Get("exports").(*goja.Object)
exports.Set("readFileSync", func(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(data), nil
})
exports.Set("writeFileSync", func(path, data string) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
return os.WriteFile(path, []byte(data), 0644)
})
exports.Set("existsSync", func(path string) bool {
_, err := os.Stat(path)
return err == nil
})
}
func init() { modules.Register(&m{}) }
Usage from JavaScript:
const fs = require("fs");
// Write and read files
fs.writeFileSync("/tmp/test.txt", "Hello World!");
const content = fs.readFileSync("/tmp/test.txt");
console.log(content); // "Hello World!"
// Check existence
if (fs.existsSync("/tmp/test.txt")) {
console.log("File exists!");
}
// modules/http/http.go
package httpmod
import (
"io"
"net/http"
"github.com/dop251/goja"
"github.com/go-go-golems/go-go-goja/modules"
)
type m struct{}
var _ modules.NativeModule = (*m)(nil)
func (m) Name() string { return "http" }
func (m) Loader(vm *goja.Runtime, moduleObj *goja.Object) {
exports := moduleObj.Get("exports").(*goja.Object)
exports.Set("get", func(url string) (map[string]interface{}, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return map[string]interface{}{
"status": resp.StatusCode,
"body": string(body),
"headers": resp.Header,
}, nil
})
}
func init() { modules.Register(&m{}) }
Usage from JavaScript:
const http = require("http");
try {
const response = http.get("https://api.github.com/users/octocat");
console.log("Status:", response.status);
const user = JSON.parse(response.body);
console.log("User:", user.login);
} catch (error) {
console.error("Request failed:", error);
}
Return Go errors directly from exported functions. The runtime automatically converts them to JavaScript Error objects:
exports.Set("divide", func(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
})
JavaScript usage with error handling:
const math = require("math");
try {
const result = math.divide(10, 2); // Returns 5
console.log(result);
} catch (error) {
console.error("Math error:", error.message);
}
Add your module to the import list in pkg/engine/runtime.go to ensure it's loaded:
import (
_ "github.com/go-go-golems/go-go-goja/modules/fs"
_ "github.com/go-go-golems/go-go-goja/modules/http"
_ "github.com/go-go-golems/go-go-goja/modules/yourmodule" // Add here
)
The blank import ensures the module's init() function runs, registering it with the module system.
If the module is user-facing, add a TypeScript descriptor next to the runtime implementation. This keeps editor completions and generated .d.ts files aligned with the actual Go exports.
Implement modules.TypeScriptDeclarer on the same module type:
import "github.com/go-go-golems/go-go-goja/pkg/tsgen/spec"
type m struct{}
var _ modules.NativeModule = (*m)(nil)
var _ modules.TypeScriptDeclarer = (*m)(nil)
func (m) TypeScriptModule() *spec.Module {
return &spec.Module{
Name: "example",
Functions: []spec.Function{
{
Name: "hello",
Params: []spec.Param{
{Name: "name", Type: spec.String()},
},
Returns: spec.String(),
},
},
}
}
The canonical declaration-generation workflow for this repository is go generate on the bun demo package:
go generate ./cmd/bun-demo
That command is defined in cmd/bun-demo/generate.go. If your new module should appear in cmd/bun-demo/js/src/types/goja-modules.d.ts, add its module name to the //go:generate go run ../gen-dts ... --module ... filter there, run generation, review the generated diff, and commit the updated .d.ts together with the module code.
For the full declaration generator reference, see:
goja-repl help typescript-declaration-generator
For asynchronous operations and Promise-based APIs, see:
glaze help async-patterns