Go Server
This guide shows how to implement an xRPC server in Go.
Prerequisites
- Define your API contract (see API Contract) and export
router - Generate Go code:
xrpc generate --targets go-server - Implement your handlers using the generated code
Note: The xRPC CLI runs on Node.js (>= 18), but the generated Go code runs on the Go runtime. The generated code is self-contained and uses Go’s standard net/http package. It implements the http.Handler interface, making it compatible with any Go HTTP framework (Gin, Echo, standard library, etc.).
Client SDKs are generated by a separate go-client target (planned).
Generated Code Structure
Code generation produces files in <output>/xrpc/:
types.go: Input/output structs matching your Zod schemasrouter.go:http.Handlerimplementation withNewRouter()functionvalidation.go: Runtime validators generated from your Zod schemas
The generated code is self-contained - it includes all HTTP handling, validation, and routing. No separate runtime libraries are needed. It uses only standard Go libraries (net/http) that are part of the Go standard library.
Basic Server Setup
The generated code provides an http.Handler that can be used with Go’s standard library or any HTTP framework:
package main
import (
"log"
"net/http"
"your-module/xrpc" // Generated code (from <output>/xrpc)
)
// Implement the greet query handler
func greetHandler(ctx *xrpc.Context, input xrpc.GreetInput) (xrpc.GreetOutput, error) {
return xrpc.GreetOutput{
Message: "Hello, " + input.Name + "!",
}, nil
}
// Implement the setGreeting mutation handler
func setGreetingHandler(ctx *xrpc.Context, input xrpc.SetGreetingInput) (xrpc.SetGreetingOutput, error) {
return xrpc.SetGreetingOutput{
Message: input.Greeting + ", " + input.Name + "!",
}, nil
}
func main() {
// Create router
router := xrpc.NewRouter().
GreetingGreet(greetHandler).
GreetingSetGreeting(setGreetingHandler)
// Use with standard library
http.Handle("/api", router)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Method naming: RPC method names use group.method (e.g., greeting.greet). The Go router exposes typed registration methods named GroupMethod (e.g., GreetingGreet).
Integration with Gin
The generated code implements Go’s standard http.Handler interface, so you can easily integrate it into any Go HTTP framework:
package main
import (
"github.com/gin-gonic/gin"
"your-module/xrpc" // Generated code (from <output>/xrpc)
)
// Implement the greet query handler
func greetHandler(ctx *xrpc.Context, input xrpc.GreetInput) (xrpc.GreetOutput, error) {
return xrpc.GreetOutput{
Message: "Hello, " + input.Name + "!",
}, nil
}
// Implement the setGreeting mutation handler
func setGreetingHandler(ctx *xrpc.Context, input xrpc.SetGreetingInput) (xrpc.SetGreetingOutput, error) {
return xrpc.SetGreetingOutput{
Message: input.Greeting + ", " + input.Name + "!",
}, nil
}
func main() {
r := gin.Default()
// Create xRPC router and register handlers
xrpcRouter := xrpc.NewRouter().
GreetingGreet(greetHandler).
GreetingSetGreeting(setGreetingHandler)
// Mount xRPC handler in your Gin server
r.POST("/api", gin.WrapH(xrpcRouter))
// Your other Gin routes work as normal
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
r.Run(":8080")
}
You can also add Gin middleware to the xRPC endpoint:
// Add middleware to xRPC endpoint
api := r.Group("/api")
api.Use(authMiddleware(), loggingMiddleware())
api.POST("", gin.WrapH(xrpcRouter))
Generated Code Structure
The generated code in <output>/xrpc/ includes:
Types (types.go): Type-safe input and output structs matching your Zod schemas:
// Generated input types
type GreetInput struct {
Name string `json:"name"`
}
type SetGreetingInput struct {
Name string `json:"name"`
Greeting string `json:"greeting"`
}
// Generated output types
type GreetOutput struct {
Message string `json:"message"`
}
type SetGreetingOutput struct {
Message string `json:"message"`
}
Router (router.go): HTTP handler implementation:
NewRouter(): Creates a new router instancerouter.<Group><Method>(handler): Registers a typed handler (e.g.,GreetingGreet)- Implements
http.Handlerinterface for framework integration
Validation: Runtime validators generated from Zod schemas validate inputs before handler execution. Output validation is not performed by the Go target yet.
Handler Signature
All handlers follow this pattern:
func handlerName(ctx *xrpc.Context, input InputType) (OutputType, error)
ctx: Extended context with middleware data (replacescontext.Context)input: Validated input matching your Zod schema- Returns: Output struct and error
Middleware Support
xRPC supports middleware for authentication, logging, cookie parsing, and other cross-cutting concerns. Middleware executes before handlers and can extend the context with typed data.
Defining Middleware in Contract
You can define middleware in your API contract:
// contract.ts
import { z } from 'zod';
import { createRouter, createEndpoint, query } from 'xrpckit';
const greeting = createEndpoint({
greet: query({
input: z.object({ name: z.string() }),
output: z.object({ message: z.string() }),
}),
});
export const router = createRouter({
middleware: [
async (req, ctx) => {
// Extract auth token
const token = req.headers.get('authorization');
return { ...ctx, userId: extractUserId(token) };
},
],
greeting,
});
Using Middleware in Go
The generated code includes middleware support. Register middleware using router.Use():
package main
import (
"net/http"
"your-module/xrpc" // Generated code
)
// Authentication middleware
func authMiddleware(ctx *xrpc.Context) *xrpc.MiddlewareResult {
token := ctx.Request.Header.Get("Authorization")
if token == "" {
return xrpc.NewMiddlewareError(fmt.Errorf("unauthorized"))
}
userId := validateToken(token)
if userId == "" {
return xrpc.NewMiddlewareError(fmt.Errorf("invalid token"))
}
// Extend context with user ID
ctx.Data["userId"] = userId
return xrpc.NewMiddlewareResult(ctx)
}
// Cookie parsing middleware
func cookieMiddleware(ctx *xrpc.Context) *xrpc.MiddlewareResult {
cookies := parseCookies(ctx.Request.Header.Get("Cookie"))
if sessionId, ok := cookies["sessionId"]; ok {
ctx.Data["sessionId"] = sessionId
}
return xrpc.NewMiddlewareResult(ctx)
}
// Handler using context data
func greetHandler(ctx *xrpc.Context, input xrpc.GreetInput) (xrpc.GreetOutput, error) {
// Access middleware data using helper functions
userId, _ := ctx.Data["userId"].(string)
return xrpc.GreetOutput{
Message: fmt.Sprintf("Hello %s! (User: %s)", input.Name, userId),
}, nil
}
func main() {
router := xrpc.NewRouter()
// Register middleware (executes in order)
router.Use(authMiddleware)
router.Use(cookieMiddleware)
// Register handlers
router.GreetingGreet(greetHandler)
http.Handle("/api", router)
http.ListenAndServe(":8080", nil)
}
Context Data Access
Context data is stored in ctx.Data as a map[string]interface{}. Access it directly:
userId, ok := ctx.Data["userId"].(string)
if !ok {
// User ID not set
}
sessionId, ok := ctx.Data["sessionId"].(string)
if ok {
// Use session ID
}
Middleware Short-Circuiting
Middleware can short-circuit the request by returning an error or response:
func authMiddleware(ctx *xrpc.Context) *xrpc.MiddlewareResult {
token := ctx.Request.Header.Get("Authorization")
if token == "" {
// Return error - request stops here
return xrpc.NewMiddlewareError(fmt.Errorf("unauthorized"))
}
// Or return a custom HTTP response
// resp := &http.Response{...}
// return xrpc.NewMiddlewareResponse(resp)
ctx.Data["userId"] = extractUserId(token)
return xrpc.NewMiddlewareResult(ctx)
}
Generated Context Type
The generated Context type includes:
type Context struct {
Request *http.Request
ResponseWriter http.ResponseWriter
Data map[string]interface{} // Extensible data map
}
This replaces context.Context in handler signatures, providing access to both the HTTP request/response and middleware-extended data.