Go-JavaScript Bridge for calling server-side Go functions from client-side JavaScript
The ForgeUI Bridge provides a seamless way to call Go functions from JavaScript without writing custom API endpoints. It supports:
- HTTP (JSON-RPC 2.0): Single and batch requests
- WebSocket: Real-time bidirectional communication
- Server-Sent Events (SSE): Server-to-client streaming
- Type Safety: Automatic parameter validation
- Security: Built-in CSRF protection, rate limiting, and authentication
- Alpine.js Integration: Magic helpers and directives
package main
import (
"github.com/xraph/forgeui/bridge"
"github.com/xraph/forgeui/router"
)
func main() {
// Create bridge
b := bridge.New()
// Register functions
b.Register("greet", func(ctx bridge.Context, input struct {
Name string `json:"name"`
}) (struct {
Message string `json:"message"`
}, error) {
return struct {
Message string `json:"message"`
}{Message: "Hello, " + input.Name}, nil
})
// Create router and enable bridge
r := router.New()
bridge.EnableBridge(r, b)
// Start server
http.ListenAndServe(":8080", r)
}<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/alpinejs@3" defer></script>
</head>
<body>
<!-- Include bridge scripts -->
<!-- Generated by bridge.BridgeScripts() -->
<div x-data="{ name: '', greeting: '' }">
<input x-model="name" placeholder="Your name">
<button @click="greeting = await $bridge.call('greet', { name })">
Greet
</button>
<p x-text="greeting"></p>
</div>
</body>
</html>Register any Go function that matches the signature:
func(ctx bridge.Context, input InputType) (OutputType, error)Example:
type CreateTodoInput struct {
Title string `json:"title"`
Done bool `json:"done"`
}
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
b.Register("createTodo", func(ctx bridge.Context, input CreateTodoInput) (*Todo, error) {
// Create todo in database
todo := &Todo{
ID: generateID(),
Title: input.Title,
Done: input.Done,
}
return todo, nil
})Customize function behavior with options:
b.Register("adminAction", handler,
bridge.RequireAuth(), // Require authentication
bridge.RequireRoles("admin"), // Require specific roles
bridge.WithTimeout(10*time.Second), // Custom timeout
bridge.WithRateLimit(10), // Rate limit (requests/min)
bridge.WithCache(5*time.Minute), // Cache results
bridge.WithDescription("Admin action"), // Documentation
)// Require authentication
b.Register("protected", handler, bridge.RequireAuth())
// Require specific roles
b.Register("adminOnly", handler,
bridge.RequireAuth(),
bridge.RequireRoles("admin", "superuser"),
)
// Access user in function
func handler(ctx bridge.Context, input InputType) (OutputType, error) {
user := ctx.User()
if user == nil {
return nil, bridge.ErrUnauthorized
}
// Use user information
userID := user.ID()
// ...
}const bridge = new ForgeBridge({
endpoint: '/api/bridge',
timeout: 30000,
csrf: document.cookie.match(/csrf_token=([^;]+)/)?.[1]
});
// Single call
const result = await bridge.call('functionName', { param: 'value' });
// Batch call
const results = await bridge.callBatch([
{ method: 'func1', params: { a: 1 } },
{ method: 'func2', params: { b: 2 } }
]);
// Streaming
const cleanup = bridge.stream('longRunning', {},
(data) => console.log('Progress:', data),
(error) => console.error('Error:', error)
);<!-- Magic helper -->
<button @click="result = await $bridge.call('functionName', { param: value })">
Call Function
</button>
<!-- Directive -->
<button x-bridge.click="greet:{name}">
Greet
</button>
<!-- Store -->
<div x-data>
<button @click="$store.bridge.call('getData', {})">
Load Data
</button>
<div x-show="$store.bridge.loading">Loading...</div>
<div x-show="$store.bridge.error" x-text="$store.bridge.error"></div>
</div>// Enable CSRF (default: enabled)
b := bridge.New(bridge.WithCSRF(true))
// Generate token
token, _ := bridge.GenerateCSRFToken()
// Set cookie
bridge.SetCSRFCookie(w, token, "csrf_token")// Global rate limit
b := bridge.New(bridge.WithDefaultRateLimit(60)) // 60 req/min
// Per-function rate limit
b.Register("func", handler, bridge.WithRateLimit(10)) // 10 req/min// Automatic validation from struct tags
type Input struct {
Email string `json:"email" validate:"email"`
Age int `json:"age" validate:"range:0,150"`
Username string `json:"username" validate:"required"`
}
// Custom validators
if err := bridge.ValidateEmail(email); err != nil {
return nil, err
}
if err := bridge.ValidateRange(age, 0, 150); err != nil {
return nil, err
}Default transport for single and batch requests.
// Automatically registered at:
// POST /api/bridge
// POST /api/bridge/batchFor real-time bidirectional communication.
// Automatically registered at:
// GET /api/bridge/ws
// Broadcast to all clients
wsHandler := bridge.NewWSHandler(b)
wsHandler.Broadcast(bridge.Event{
Type: "notification",
Data: "Hello everyone!",
})
// Send to specific user
wsHandler.SendToUser(userID, bridge.Event{
Type: "message",
Data: "Personal message",
})For server-to-client streaming.
// Automatically registered at:
// GET /api/bridge/stream?method=funcName¶ms=...Execute code before/after function calls:
// Before call
b.GetHooks().Register(bridge.BeforeCall, func(ctx bridge.Context, data bridge.HookData) {
log.Printf("Calling %s with params: %v", data.FunctionName, data.Params)
})
// After call
b.GetHooks().Register(bridge.AfterCall, func(ctx bridge.Context, data bridge.HookData) {
log.Printf("Function %s took %dμs", data.FunctionName, data.Duration)
})
// On error
b.GetHooks().Register(bridge.OnError, func(ctx bridge.Context, data bridge.HookData) {
log.Printf("Error in %s: %v", data.FunctionName, data.Error)
})
// On success
b.GetHooks().Register(bridge.OnSuccess, func(ctx bridge.Context, data bridge.HookData) {
log.Printf("Function %s succeeded: %v", data.FunctionName, data.Result)
})// Enable caching
b := bridge.New(bridge.WithCache(true))
// Cache specific function results
b.Register("expensiveOp", handler,
bridge.WithCache(10*time.Minute), // Cache for 10 minutes
)List all registered functions and their metadata:
// Automatically registered at:
// GET /api/bridge/functions
// Programmatic access
functions := b.ListFunctionInfo()
for _, fn := range functions {
fmt.Printf("Function: %s\n", fn.Name)
fmt.Printf(" Description: %s\n", fn.Description)
fmt.Printf(" Requires Auth: %v\n", fn.RequireAuth)
fmt.Printf(" Input Type: %s\n", fn.TypeInfo.InputType)
fmt.Printf(" Output Type: %s\n", fn.TypeInfo.OutputType)
}b := bridge.New(
bridge.WithTimeout(30*time.Second), // Default timeout
bridge.WithMaxBatchSize(10), // Max batch size
bridge.WithCSRF(true), // Enable CSRF
bridge.WithCORS(true), // Enable CORS
bridge.WithAllowedOrigins("*"), // Allowed origins
bridge.WithDefaultRateLimit(60), // Default rate limit
bridge.WithCache(true), // Enable caching
)// Inline scripts
html := bridge.BridgeScripts(bridge.ScriptConfig{
Endpoint: "/api/bridge",
CSRFToken: csrfToken,
IncludeAlpine: true,
})
// External scripts
html := bridge.BridgeScriptsExternal(bridge.ScriptConfig{
Endpoint: "/api/bridge",
CSRFToken: csrfToken,
IncludeAlpine: true,
})func handler(ctx bridge.Context, input Input) (Output, error) {
if input.Invalid {
return nil, bridge.NewError(bridge.ErrCodeBadRequest, "Invalid input")
}
// Or use predefined errors
if !authorized {
return nil, bridge.ErrUnauthorized
}
// Custom error with details
return nil, bridge.NewError(bridge.ErrCodeBadRequest, "Validation failed", map[string]any{
"field": "email",
"reason": "invalid format",
})
}try {
const result = await bridge.call('functionName', params);
} catch (err) {
if (err instanceof BridgeError) {
console.log('Error code:', err.code);
console.log('Error message:', err.message);
console.log('Error data:', err.data);
}
}func TestMyFunction(t *testing.T) {
b := bridge.New()
b.Register("myFunc", myHandler)
req := httptest.NewRequest("GET", "/", nil)
ctx := bridge.NewContext(req)
result, err := b.Call(ctx, "myFunc", json.RawMessage(`{"input":"value"}`))
if err != nil {
t.Fatal(err)
}
// Assert result
}func TestBridgeIntegration(t *testing.T) {
b := bridge.New(bridge.WithCSRF(false))
b.Register("test", handler)
handler := bridge.NewHTTPHandler(b)
req := bridge.Request{
JSONRPC: "2.0",
ID: "1",
Method: "test",
Params: json.RawMessage(`{}`),
}
body, _ := json.Marshal(req)
httpReq := httptest.NewRequest("POST", "/api/bridge", bytes.NewReader(body))
w := httptest.NewRecorder()
handler.ServeHTTP(w, httpReq)
// Assert response
}- Overhead: <5ms per request (excluding function execution)
- Throughput: 1000+ req/s on modest hardware
- Memory: Minimal footprint with connection pooling
- JavaScript Bundle: <3KB minified + gzipped
- Use meaningful function names:
createUsernotfunc1 - Validate inputs: Always validate user inputs
- Set timeouts: Use appropriate timeouts for long-running functions
- Enable rate limiting: Protect against abuse
- Use authentication: Protect sensitive functions
- Cache expensive operations: Use caching for expensive computations
- Handle errors gracefully: Return descriptive error messages
- Document functions: Use
WithDescription()for API documentation - Test thoroughly: Write unit and integration tests
- Monitor performance: Use hooks for logging and monitoring
MIT License - See LICENSE file for details