This document details the implementation of REST API handling in Synapse Go, including the router service, API versioning, path and query parameter handling, CORS support, and Swagger documentation generation.
The core of API handling in Synapse Go is the router service, which is responsible for accepting HTTP requests, routing them to the appropriate handlers, and returning responses.
flowchart TD A[HTTP Request] --> B[Router Service] B --> C[API Subrouting] B --> D[Swagger Endpoint] C --> E[CORS Middleware] E --> F[API Handlers] classDef request fill:#f9e8a0,stroke:#e6b800,stroke-width:1px classDef service fill:#a0d8f9,stroke:#0073e6,stroke-width:1px classDef routing fill:#a0f9a5,stroke:#00b33c,stroke-width:1px classDef middleware fill:#e2a0f9,stroke:#9900cc,stroke-width:1px classDef handlers fill:#f9a0a0,stroke:#cc0000,stroke-width:1px class A request class B,D service class C routing class E middleware class F handlers
The router service is implemented in internal/pkg/core/router/service.go and is responsible for:
type RouterService struct {
server *http.Server
router *mux.Router
hostname string
listenAddress string
logger *slog.Logger
level slog.Level
apis map[string]artifacts.API
}
The router service is initialized with a listen address (port) and hostname:
func NewRouterService(listenAddr string, hostname string) *RouterService {
router := mux.NewRouter().StrictSlash(true)
server := &http.Server{
Addr: listenAddr,
Handler: router,
}
return &RouterService{
server: server,
router: router,
hostname: hostname,
listenAddress: listenAddr,
logger: loggerfactory.GetLogger("router"),
apis: make(map[string]artifacts.API),
}
}
The server is started in a goroutine to avoid blocking:
func (r *RouterService) StartServer(ctx context.Context) { wg := ctx.Value(utils.WaitGroupKey).(*sync.WaitGroup) wg.Add(1) go func() { defer wg.Done() r.logger.Info("Starting HTTP server", "address", r.server.Addr) if err := r.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { r.logger.Error("HTTP server error", "error", err) } }() go func() { <-ctx.Done() r.logger.Info("Shutting down HTTP server...") // Create a timeout context for graceful shutdown shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := r.server.Shutdown(shutdownCtx); err != nil { r.logger.Error("HTTP server shutdown error", "error", err) } }() }
This implementation:
The router service implements graceful shutdown with a configurable timeout:
func (r *RouterService) StopServer() {
r.logger.Info("HTTP server shutdown complete")
}
This method is called by the main application after waiting for all goroutines to complete.
When an API is deployed, it is registered with the router service:
func (r *RouterService) RegisterAPI(ctx context.Context, api artifacts.API) error { r.apis[api.Name] = api // Calculate base path basePath := api.calculateBasePath() // Create a subrouter for this API apiRouter := r.router.PathPrefix(basePath).Subrouter() // Register Swagger endpoints r.registerSwaggerEndpoints(apiRouter, api) // Register API resources with CORS if enabled for _, resource := range api.Resources { r.registerResource(apiRouter, resource, api.CORSConfig) } r.logger.Info("Registered API", "name", api.Name, "basePath", basePath) return nil }
This method:
Synapse Go supports API versioning through two mechanisms:
func (api *API) calculateBasePath() string { basePath := api.Context // Add version to path if specified if api.Version != "" { if api.VersionType == "context" { // Replace {version} placeholder with actual version basePath = strings.Replace(basePath, "{version}", api.Version, -1) } } return basePath }
For example, with context-based versioning:
/healthcare/{version}/services1.0/healthcare/1.0/servicesThe URI template in each resource is parsed to extract path parameters and query parameters:
func (r *Resource) parseURITemplate(uriTemplate string) (artifacts.URITemplateInfo, error) { info := artifacts.URITemplateInfo{ FullTemplate: uriTemplate, PathTemplate: "", PathParameters: []string{}, QueryParams: map[string]string{}, } // Split on the question mark to separate path from query parameters parts := strings.SplitN(uriTemplate, "?", 2) info.PathTemplate = parts[0] // Extract path parameters pathRegex := regexp.MustCompile(`\{([^/]+?)\}`) matches := pathRegex.FindAllStringSubmatch(info.PathTemplate, -1) for _, match := range matches { if len(match) > 1 { info.PathParameters = append(info.PathParameters, match[1]) } } // Extract query parameters if they exist if len(parts) > 1 { queryPart := parts[1] queryParams := strings.Split(queryPart, "&") for _, param := range queryParams { paramParts := strings.SplitN(param, "=", 2) if len(paramParts) > 1 { paramName := paramParts[0] paramValue := paramParts[1] // Extract parameter name from {name} format paramRegex := regexp.MustCompile(`\{([^{}]+)\}`) paramMatch := paramRegex.FindStringSubmatch(paramValue) if len(paramMatch) > 1 { info.QueryParams[paramName] = paramMatch[1] } } } } return info, nil }
This parsing enables:
Each API resource is registered with the router:
func (r *RouterService) registerResource(router *mux.Router, resource artifacts.Resource, corsConfig artifacts.CORSConfig) { // Create route with path parameters route := router.Path(resource.URITemplate.PathTemplate) // Restrict to specified HTTP methods route = route.Methods(resource.Methods...) // Create handler handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { r.handleRequest(w, req, resource) }) // Apply CORS middleware if enabled if corsConfig.Enabled { handler = cors.CORSMiddleware(handler, corsConfig) } // Register the handler route.Handler(handler) }
This registration process:
Synapse Go provides comprehensive CORS (Cross-Origin Resource Sharing) support through middleware:
func CORSMiddleware(handler http.Handler, config artifacts.CORSConfig) http.Handler { // Skip CORS handling if disabled if !config.Enabled { return handler } // Convert our config to rs/cors options options := cors.Options{ AllowedOrigins: config.AllowOrigins, AllowedMethods: config.AllowMethods, AllowedHeaders: config.AllowHeaders, ExposedHeaders: config.ExposeHeaders, AllowCredentials: config.AllowCredentials, MaxAge: config.MaxAge, } // Create the cors handler corsHandler := cors.New(options) // Use the handler as middleware return corsHandler.Handler(handler) }
This middleware:
CORS is configured in the API definition:
<api name="HealthcareAPI" context="/healthcare/{version}/services" version="1.0" version-type="context"> <cors enabled="true" allow-origins="https://example.com,https://app.example.com" allow-methods="GET,POST,PUT,DELETE,PATCH,OPTIONS" allow-headers="Content-Type,Authorization,X-Requested-With,Accept" expose-headers="X-Request-ID,X-Response-Time" allow-credentials="true" max-age="3600" /> <!-- Resources --> </api>
This configuration is parsed during artifact deployment:
func (api *API) Unmarshal(xmlData string, position artifacts.Position) (artifacts.API, error) { // ... case "cors": // Parse CORS element corsElem := &CORSElement{} if err := decoder.DecodeElement(corsElem, &elem); err != nil { return artifacts.API{}, fmt.Errorf("error decoding CORS element: %w", err) } // Parse the CORS configuration cors := artifacts.DefaultCORSConfig() // Enable CORS if specified if corsElem.Enabled == "true" { cors.Enabled = true } // Parse allowed origins if corsElem.AllowOrigins != "" { cors.AllowOrigins = strings.Split(corsElem.AllowOrigins, ",") for i := range cors.AllowOrigins { cors.AllowOrigins[i] = strings.TrimSpace(cors.AllowOrigins[i]) } } // Parse other CORS settings // ... newAPI.CORSConfig = cors // ... }
Synapse Go automatically generates Swagger/OpenAPI documentation for each API:
func (r *RouterService) registerSwaggerEndpoints(router *mux.Router, api artifacts.API) { // Register JSON endpoint router.Path("/swagger.json").Methods("GET").HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if err := api.ServeSwaggerJSON(w, r.hostname, r.listenAddress); err != nil { http.Error(w, "Error generating Swagger JSON", http.StatusInternalServerError) } }) // Register YAML endpoint router.Path("/swagger.yaml").Methods("GET").HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if err := api.ServeSwaggerYAML(w, r.hostname, r.listenAddress); err != nil { http.Error(w, "Error generating Swagger YAML", http.StatusInternalServerError) } }) }
The OpenAPI specification is generated dynamically based on the API definition:
func (api *API) GenerateOpenAPISpec(hostname string, port string) (map[string]interface{}, error) { // --- 1. Basic OpenAPI Structure --- spec := make(map[string]interface{}) spec["openapi"] = "3.0.3" // Specify OpenAPI version // --- 2. Info Object --- info := make(map[string]interface{}) title := api.Name if title == "" { title = "API Documentation" } info["title"] = title info["version"] = api.Version spec["info"] = info // --- 3. Servers Object --- basePath := api.calculateBasePath() serverURL := fmt.Sprintf("http://%s%s%s", hostname, port, basePath) servers := []map[string]interface{}{ { "url": serverURL, }, } spec["servers"] = servers // --- 4. Paths Object --- paths := make(map[string]interface{}) for _, resource := range api.Resources { // Generate path item for each resource // ... } spec["paths"] = paths return spec, nil }
This process:
When a request is received, it flows through the following components:
func (r *RouterService) handleRequest(w http.ResponseWriter, req *http.Request, resource artifacts.Resource) { // Create context for this request ctx := req.Context() // Execute inbound sequence if defined if resource.InSequence.MediatorList != nil && len(resource.InSequence.MediatorList) > 0 { // Create message from request message := &domain.Message{ Payload: req, } // Get the mediation engine mediator := ctx.Value(utils.MediationEngineKey).(ports.InboundMessageMediator) // Execute the sequence result, err := resource.InSequence.Mediate(ctx, message, mediator) if err != nil { // Handle error // ... return } // Process result // ... } }
The API implementation in Synapse Go provides:
This implementation aligns with the hexagonal architecture principles by: