Getting Started with RonyKIT
May 31, 2026 · View on GitHub
This guide walks you through building API servers with RonyKIT. No prior RonyKIT experience required — basic Go knowledge is enough.
Table of Contents
- Installation
- Your First Server
- Defining Routes and Handlers
- Request Binding
- State Management
- Middleware
- Streaming Handlers
- Error Handling
- API Docs
- Client Stubs
- File Uploads and Raw Bodies
- Testing
- Server Options Reference
- Context Helpers Reference
- Production Checklist
Installation
You need Go 1.25+. Install the scaffolding tool:
go install github.com/clubpay/ronykit/ronyup@latest
To scaffold a complete project with DI, config, migrations, and repo layers:
ronyup setup workspace -r ./my-api -m github.com/you/my-api
cd my-api
ronyup setup feature -p users -n users -t service
This creates a feature module at feature/users/ (default layout).
Or start from scratch by adding the rony package directly:
go get github.com/clubpay/ronykit/rony@latest
Your First Server
A complete, working server in a single file:
package main
import (
"context"
"os"
"github.com/clubpay/ronykit/rony"
)
type GreetRequest struct {
Name string `json:"name"`
}
type GreetResponse struct {
Message string `json:"message"`
}
func Greet(ctx *rony.SUnaryCtx, in GreetRequest) (*GreetResponse, error) {
return &GreetResponse{Message: "Hello, " + in.Name + "!"}, nil
}
func main() {
srv := rony.NewServer(
rony.WithServerName("greeter"),
rony.WithVersion("v1.0.0"),
rony.Listen(":8080"),
rony.WithAPIDocs("/docs"),
rony.UseSwaggerUI(),
)
rony.Setup(srv, "GreeterService", rony.EmptyState(),
rony.WithUnary(Greet, rony.GET("/hello/{name}")),
)
_ = srv.Run(context.Background(), os.Interrupt, os.Kill)
}
Run it and try it:
go run main.go
curl http://localhost:8080/hello/World
# {"message":"Hello, World!"}
Open http://localhost:8080/docs for auto-generated Swagger UI.
What happened:
rony.NewServer(...)creates a server with an HTTP gateway on port 8080.rony.Setup(...)registers a service with one contract (theGreethandler bound toGET /hello/{name}).srv.Run(...)starts the server and blocks until a shutdown signal arrives.
Defining Routes and Handlers
Unary handlers (request -> response)
The most common handler type. Takes a typed request, returns a typed response:
func GetUser(ctx *rony.SUnaryCtx, in GetUserRequest) (*GetUserResponse, error) {
// process request and return response
return &GetUserResponse{...}, nil
}
rony.SUnaryCtx is a shorthand for stateless handlers. If you need shared state, use *rony.UnaryCtx[S, A] (see State Management).
Binding handlers to routes
Register handlers with route helpers during rony.Setup:
rony.Setup(srv, "UserService", rony.EmptyState(),
rony.WithUnary(CreateUser, rony.POST("/users")),
rony.WithUnary(GetUser, rony.GET("/users/{id}")),
rony.WithUnary(UpdateUser, rony.PUT("/users/{id}")),
rony.WithUnary(DeleteUser, rony.DELETE("/users/{id}")),
rony.WithUnary(ListUsers, rony.GET("/users")),
)
Available route helpers: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, ALL, and the generic REST(method, path).
Route options
Customize individual routes for documentation and behavior:
rony.WithUnary(GetUser,
rony.GET("/users/{id}", rony.UnaryName("GetUser")),
rony.UnaryHeader(rony.RequiredHeader("Authorization")),
)
Grouping related handlers
Use SetupOptionGroup to organize handlers by domain:
func userHandlers() rony.SetupOption[rony.EMPTY, rony.NOP] {
return rony.SetupOptionGroup(
rony.WithUnary(CreateUser, rony.POST("/users", rony.UnaryName("CreateUser"))),
rony.WithUnary(GetUser, rony.GET("/users/{id}", rony.UnaryName("GetUser"))),
rony.WithUnary(ListUsers, rony.GET("/users", rony.UnaryName("ListUsers"))),
)
}
func orderHandlers() rony.SetupOption[rony.EMPTY, rony.NOP] {
return rony.SetupOptionGroup(
rony.WithUnary(CreateOrder, rony.POST("/orders", rony.UnaryName("CreateOrder"))),
rony.WithUnary(GetOrder, rony.GET("/orders/{id}", rony.UnaryName("GetOrder"))),
)
}
rony.Setup(srv, "MyAPI", rony.EmptyState(),
userHandlers(),
orderHandlers(),
)
Request Binding
RonyKIT automatically binds path parameters, query parameters, and request body to your input struct fields. It matches fields using the json tag.
type SearchRequest struct {
Category string `json:"category"` // from path /{category}, query ?category=, or body
Query string `json:"query"` // from query ?query= or body
Page *int32 `json:"page"` // optional — nil when not provided
}
Priority: path params > query params > body fields.
Tips:
- Use pointer types for optional fields — they stay
nilwhen not provided. - Read request headers with
ctx.GetInHdr("X-Request-Id")— headers aren't bound to struct fields. - Keep tag names consistent across path, query, and body.
State Management
If handlers need to share mutable data, rony provides a built-in reducer pattern.
Define your state
type Counter struct {
sync.Mutex
Count int
}
func (c *Counter) Name() string { return "Counter" }
func (c *Counter) Reduce(action string) error {
switch action {
case "increment":
c.Count++
case "decrement":
if c.Count <= 0 {
return fmt.Errorf("count cannot go below zero")
}
c.Count--
default:
return fmt.Errorf("unknown action: %s", action)
}
return nil
}
When your state implements sync.Locker, ReduceState automatically locks around mutations.
Use it in a handler
func Count(ctx *rony.UnaryCtx[*Counter, string], in CountRequest) (*CountResponse, error) {
res := &CountResponse{}
err := ctx.ReduceState(in.Action, func(s *Counter, err error) error {
if err != nil {
return err
}
res.Count = s.Count
return nil
})
if err != nil {
return nil, err
}
return res, nil
}
Register with initial state
rony.Setup(srv, "CounterService",
rony.ToInitiateState[*Counter, string](&Counter{Count: 0}),
rony.WithUnary(Count, rony.GET("/count/{action}")),
)
When you don't need state, use rony.EmptyState() and the rony.SUnaryCtx shorthand.
Advice: Keep state small. For heavy shared resources (database pools, caches), use dependency injection or pass them through handler closures.
Middleware
Middleware runs before your handlers. Use it for auth, logging, rate limiting, or request shaping.
Stateless middleware
The most common kind — wraps kit.HandlerFunc:
func LogMiddleware(ctx *kit.Context) {
start := time.Now()
ctx.Next()
log.Printf("%s %s took %v", ctx.Route(), ctx.Conn().ClientIP(), time.Since(start))
}
Register at service level:
rony.Setup(srv, "MyService", rony.EmptyState(),
rony.WithMiddleware[rony.EMPTY, rony.NOP](LogMiddleware),
rony.WithUnary(MyHandler, rony.GET("/items")),
)
Or at contract level:
rony.WithUnary(MyHandler,
rony.GET("/items"),
rony.UnaryMiddleware(AuthRequired),
)
Prefer stateless middleware unless you truly need shared state access.
Streaming Handlers
For server-push or bidirectional messaging (WebSocket), use streaming handlers:
type ChatIn struct {
Text string `json:"text"`
}
type ChatOut struct {
Text string `json:"text"`
}
func Chat(ctx *rony.SStreamCtx[ChatOut], in ChatIn) error {
ctx.Push(ChatOut{Text: "echo: " + in.Text})
return nil
}
rony.Setup(srv, "ChatService", rony.EmptyState(),
rony.WithStream(Chat, rony.RPC("chatMessage")),
)
- Use
ctx.Push(msg)to send messages to the client. - Streaming is typically used over WebSocket via the
fasthttpgateway.
Error Handling
Return structured errors using the rony/errs package:
import "github.com/clubpay/ronykit/rony/errs"
func GetUser(ctx *rony.SUnaryCtx, in GetUserRequest) (*GetUserResponse, error) {
if in.ID == "" {
return nil, errs.B().
Code(errs.InvalidArgument).
Msg("USER_ID_REQUIRED").
Err()
}
user, err := userRepo.Get(ctx.Context(), in.ID)
if err != nil {
return nil, errs.B().
Code(errs.Internal).
Msg("FAILED_TO_GET_USER").
Cause(err).
Err()
}
return &GetUserResponse{User: user}, nil
}
Error codes map to HTTP status codes automatically:
| Code | HTTP Status |
|---|---|
errs.InvalidArgument | 400 |
errs.Unauthenticated | 401 |
errs.PermissionDenied | 403 |
errs.NotFound | 404 |
errs.AlreadyExists | 409 |
errs.Internal | 500 |
Override error serialization globally:
rony.NewServer(
rony.WithErrorHandler(func(ctx *kit.Context, err error) {
ctx.SetStatusCode(400)
ctx.Out().SetMsg(
errs.B().Cause(err).Code(errs.InvalidArgument).Msg("COULD_NOT_PARSE_PAYLOAD").Err(),
).Send()
}),
)
API Docs
RonyKIT generates and serves OpenAPI documentation from your handler types.
Serve docs at runtime
srv := rony.NewServer(
rony.WithAPIDocs("/docs"),
rony.UseSwaggerUI(), // or UseRedocUI() or UseScalarUI()
)
Enrich docs with metadata
rony.WithUnary(Search,
rony.GET("/search/{category}"),
rony.UnaryInputMeta(
desc.WithField("category", desc.FieldMeta{
Description: "Product category",
Enum: []string{"electronics", "books", "clothing"},
}),
),
rony.UnaryHeader(rony.RequiredHeader("Authorization")),
)
Export to file
_ = srv.GenDocFile(context.Background(), "openapi.json")
Client Stubs
Generate type-safe clients in Go or TypeScript from your service definitions.
Go client
import "github.com/clubpay/ronykit/stub/stubgen"
svcs := srv.ExportDesc()
stubgen.New(
stubgen.WithGenEngine(stubgen.NewGolangEngine(stubgen.GolangConfig{
PkgName: "myclient",
})),
stubgen.WithTags("json"),
stubgen.WithFolderName("myclient"),
stubgen.WithStubName("myService"),
).MustGenerate(svcs...)
TypeScript client
stubgen.New(
stubgen.WithGenEngine(stubgen.NewTypescriptEngine(stubgen.TypescriptConfig{
GenerateSWR: true,
})),
stubgen.WithTags("json"),
stubgen.WithFolderName("myclient-ts"),
stubgen.WithStubName("myService"),
).MustGenerate(svcs...)
File Uploads and Raw Bodies
File uploads (multipart forms)
func Upload(ctx *rony.SUnaryCtx, in kit.MultipartFormMessage) (*kit.RawMessage, error) {
form := in.GetForm()
for name, files := range form.File {
_ = name
_ = files
}
empty := kit.RawMessage{}
return &empty, nil
}
rony.Setup(srv, "FileService", rony.EmptyState(),
rony.WithUnary(Upload, rony.POST("/upload")),
)
Raw request/response bodies
func RawEcho(ctx *rony.SUnaryCtx, in kit.RawMessage) (*kit.RawMessage, error) {
return &in, nil
}
HTTP redirects
func Redirect(ctx *rony.SUnaryCtx, in RedirectRequest) (*rony.EMPTY, error) {
if rc, ok := ctx.RESTConn(); ok {
rc.Redirect(307, in.URL)
}
return nil, nil
}
Testing
Run the test suite
make setup # install tools — run once
make test # run tests across all modules
make lint # lint all modules
To test a single module:
cd rony && go test ./...
Test your handlers
Handlers are regular Go functions with typed inputs and outputs — test them directly:
func TestGreet(t *testing.T) {
resp, err := Greet(testCtx, GreetRequest{Name: "World"})
assert.NoError(t, err)
assert.Equal(t, "Hello, World!", resp.Message)
}
For integration tests, use generated Go client stubs to call your endpoints end-to-end.
Server Options Reference
| Option | Description |
|---|---|
Listen(":8080") | Bind address for the HTTP gateway |
WithServerName("name") | Server name (appears in docs and logs) |
WithVersion("v1.0.0") | API version |
WithCORS(config) | Cross-origin resource sharing |
WithCompression(level) | Response compression (gzip/zstd) |
WithAPIDocs("/docs") | Serve OpenAPI docs at the given path |
UseSwaggerUI() | Use Swagger UI instead of the default ReDoc |
UseScalarUI() | Use Scalar UI for docs |
WithTracer(tracer) | Plug in a distributed tracer |
WithLogger(logger) | Plug in a structured logger |
WithPrefork() | Multi-process mode for higher throughput |
WithShutdownTimeout(d) | Graceful shutdown timeout |
WithErrorHandler(fn) | Override global error serialization |
WithGlobalHandlers(h...) | Global middleware for all routes |
Context Helpers Reference
| Method | Description |
|---|---|
ctx.GetInHdr("key") | Read a request header |
ctx.SetOutHdr("key", "val") | Set a response header |
ctx.Conn() | Access the underlying connection |
ctx.Context() | Get the context.Context for the request |
ctx.SetUserContext(c) | Replace the request context |
ctx.RESTConn() | Access REST-specific helpers (method, path, redirect) |
ctx.StopExecution() | Short-circuit remaining middleware |
ctx.KitCtx() | Access the underlying kit.Context |
Production Checklist
- Error handling — consistent error codes from
rony/errsacross all handlers - Input validation — DTOs properly tagged; optional fields use pointer types
- API docs — enriched with field metadata, enums, and descriptions
- CORS — configured with
rony.WithCORS(rony.CORSConfig{...}) - Compression — enabled with
rony.WithCompression(...) - Auth middleware — in place for protected endpoints
- Observability — tracer and logger wired with
rony.WithTracer/rony.WithLogger - Timeouts — set at reverse proxy and service layer with
rony.WithShutdownTimeout - Client stubs — generated and distributed to consumers
- Tests —
make testandmake lintboth green
Next Steps
- Cookbook — production patterns for auth, pagination, validation, and more
- ronyup Guide — scaffolding CLI and MCP server
- Architecture — how RonyKit works internally
- Advanced: Kit — low-level toolkit for custom gateways