Kyrux
Back

APIs (REST, GraphQL, gRPC, SOAP)

🧪 Experimental — part of the core layer (kyrux/core), labeled "experimental" by the framework's own USE.md. The API may change between versions without notice.

All four protocols live as adapters (core/adapters/...) on top of core.Core — a new orchestrator, parallel to the traditional bootstrap, with its own lifecycle (Init → Configure → Start → Shutdown).

import "kyrux/core"

c := core.New()
// ... register REST/GraphQL/gRPC/SOAP ...
c.Run()            // Start on every activated module
defer c.Shutdown()  // Shutdown in reverse order

Two ways to activate a module:

  • Sugar — ready-made namespaces (core.API.REST(addr), core.Auth.OAuth2(...)) for lightweight/stdlib-only protocols.
  • core.UseModule[T] — generic, used by the protocols with heavy dependencies (GraphQL, gRPC), which don't get sugar on purpose, so those dependencies aren't pulled in for people who don't use them.

None of the four read environment variables — address/port/options are always passed as Go arguments in your own code.

REST — core/adapters/restapi

Not a new protocol — it's the same core/router.Router as always, just running on core.Core's own *http.Server/port, orchestrated by Run()/Shutdown() instead of fw.Run().

import "kyrux/core"

c := core.New()
r, err := c.API.REST(":8080") // *router.Router — same API as always

r.Handle("GET /ping", func(ctx *router.Context) {
    ctx.JSON(200, map[string]string{"pong": "kyrux"})
})

c.Run()

GraphQL — core/adapters/apigraphql

No sugar (heavy dependency — graphql-go brings its own parser and template engine). You build the schema with graphql-go/graphql's own API and hand the finished schema to the adapter.

import (
    "github.com/graphql-go/graphql"
    "kyrux/core"
    "kyrux/core/adapters/apigraphql"
)

queryType := graphql.NewObject(graphql.ObjectConfig{
    Name: "Query",
    Fields: graphql.Fields{
        "version": &graphql.Field{
            Type: graphql.String,
            Resolve: func(p graphql.ResolveParams) (any, error) {
                return "kyrux-core", nil
            },
        },
    },
})
schema, _ := graphql.NewSchema(graphql.SchemaConfig{Query: queryType})

c := core.New()
_, err := core.UseModule[*graphql.Schema](c, apigraphql.New(":8081", schema), "api.graphql.principal")
c.Run()

Serves POST / with a {"query": "..."} body. The GraphiQL IDE is enabled by default, with no toggle to disable it. Runs on its own *http.Server — it's not mounted on the REST router.

gRPC — core/adapters/apigrpc

Also no sugar (HTTP/2 + protobuf runtime). The adapter only hosts the *grpc.Server — you generate your service code from a .proto (protoc

  • protoc-gen-go + protoc-gen-go-grpc) and register it the normal gRPC way.
import (
    "kyrux/core"
    "kyrux/core/adapters/apigrpc"
)

c := core.New()
srv, err := core.UseModule[*grpc.Server](c, apigrpc.New(":9090"), "api.grpc.principal")
pb.RegisterGreeterServer(srv, &myImplementation{}) // standard gRPC registration

// gRPC and REST coexist on the same Core, different ports:
r, _ := c.API.REST(":8080")

c.Run()

Shutdown calls GracefulStop() with no timeout — add your own watchdog if you need a time limit.

SOAP — core/soap + core/adapters/soapclient

A hand-rolled implementation (encoding/xml + net/http, no third-party library — Go's SOAP ecosystem is weak). Aimed at cases like legacy/ government webservices.

Server — not a separate port, it's an http.Handler you mount on the REST router:

import (
    "kyrux/core"
    "kyrux/core/soap"
)

server := soap.NewServer()
server.Handle("SumRequest", func(ctx context.Context, requestXML []byte) ([]byte, error) {
    // parse requestXML, build the response
    return responseXML, nil
})

c := core.New()
r, _ := c.API.REST(":8080")
r.HandlePrefix("/soap", server) // same pattern used to mount Prometheus metrics

c.Run()

Routing is by the root element name inside <Body> — pragmatic, not a "formal" WSDL/SOAPAction-based dispatch.

Client, raw or via Core sugar:

// Raw:
client := soap.NewClient(endpoint, httpClient)

// Sugar (zero cost — it's pure stdlib):
client, err := c.API.SOAPClient("principal", endpoint)

var resp sumResponse
err = client.Call(ctx, "Sum", sumRequest{A: 4, B: 5}, &resp)
// a server-side error becomes a *soap.Fault (errors.As)

None of them are read from .env

Like NoSQL, none of the four protocols follow an API_REST_ADDR-style convention — address, schema, grpc.Server options: everything is passed as a Go argument in your own code, when you build the core.Core.