Go Server

This guide shows how to implement an xRPC server in Go.

Prerequisites

  1. Define your API contract (see API Contract) and export router
  2. Generate Go code: xrpc generate --targets go-server
  3. 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 schemas
  • router.go: http.Handler implementation with NewRouter() function
  • validation.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 instance
  • router.<Group><Method>(handler): Registers a typed handler (e.g., GreetingGreet)
  • Implements http.Handler interface 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 (replaces context.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.