A dead-simple Go library for building gRPC + REST microservices with grpc-gateway.
- gRPC + REST - Serve both protocols from a single service definition
- Custom Content Types - Support for form-urlencoded, XML, binary, multipart, and more
- Health checks - Built-in
/healthzand/readyzendpoints - Prometheus metrics - Built-in
/metricsendpoint - Swagger UI - Serve OpenAPI documentation at
/swagger/ - Authentication - Decorator pattern for protecting endpoints
- Graceful shutdown - Clean shutdown with configurable timeout
- Zero boilerplate - Focus on your business logic, not infrastructure
go get github.com/gyozatech/grpckitUse the included generator script to create a complete service boilerplate:
# Download the script (or clone the repo)
curl -O https://raw.githubusercontent.com/gyozatech/grpckit/main/scripts/create-service.sh
chmod +x create-service.sh
# Generate a new service (both formats work)
./create-service.sh --name=user --module=github.com/myorg/user-service
./create-service.sh --name user --module github.com/myorg/user-service --output ./user-service
# Use an existing proto file
./create-service.sh --proto=https://example.com/api.proto --module=github.com/myorg/api-service
./create-service.sh --proto=./path/to/service.proto --module=github.com/myorg/my-service
# With external proto and explicit go_package
./create-service.sh \
--proto=https://git.example.com/repo/-/raw/main/api/v1/service.proto \
--module=github.com/myorg/my-service \
--go-package=git.example.com/repo/clients/api/v1/api-go \
--grpckit-version=v0.0.2
# With custom ports and swagger (fetched at build time)
./create-service.sh \
--name=user \
--module=github.com/myorg/user-service \
--grpc-port=50051 \
--http-port=8081 \
--swagger=https://git.example.com/org/api/-/raw/main/swagger.jsonWhen --swagger is specified with a URL:
- Your
main.gosimply usesgrpckit.WithSwagger("https://...")- clean, no boilerplate - The Makefile
swaggertarget fetches the spec and generatesswagger_gen.go swagger_gen.gohandles the embedding (gitignored - you never see or edit it)- At runtime, the swagger is served from memory
Your code stays clean:
grpckit.WithSwagger("https://git.example.com/api/-/raw/v1.0.0/swagger.json")The embedding magic is hidden in swagger_gen.go (auto-generated, gitignored).
Note: Ensure the swagger spec version matches your imported proto version. If your swagger URL contains a branch name (e.g., main), update it to a specific version tag that corresponds to the proto module version in your go.sum.
Note: If you forget to run make swagger, the server still starts but /swagger/ returns 404 with a helpful message.
# Build fetches swagger automatically (it's a dependency of build)
make build
# Or fetch swagger explicitly
make swaggerFor private repos, make swagger automatically detects the git provider and retrieves tokens:
| Provider | Auto-Detection | Token Source | Setup Command |
|---|---|---|---|
| GitHub | github.com, raw.githubusercontent.com |
gh auth token |
gh auth login |
| GitLab | gitlab.com, /-/raw/ pattern |
glab config get token |
glab auth login --hostname <host> |
| Bitbucket | bitbucket.org |
$BITBUCKET_TOKEN env var |
export BITBUCKET_TOKEN=<token> |
| Other | - | $SWAGGER_TOKEN env var |
export SWAGGER_TOKEN=<token> |
You can also explicitly specify the provider:
./create-service.sh --swagger=https://... --swagger-git-provider=gitlabSecurity: Tokens are retrieved at build time only from CLI configs or environment variables. They are never written to any file or committed.
All arguments support both --arg value and --arg=value formats.
| Argument | Required | Description |
|---|---|---|
--name, -n |
Yes* | Service name (e.g., "user", "order"). *Not required if --proto is specified |
--module, -m |
Yes | Go module path |
--output, -o |
No | Output directory (default: ./<name>-service) |
--proto, -p |
No | URL or path to an existing proto file to use |
--go-package |
No | Go import path for the proto's generated code. Required if proto doesn't have go_package option |
--grpckit-version |
No | grpckit version (e.g., "v0.0.2"). If not specified, uses placeholder for go mod tidy |
--swagger |
No | URL to swagger JSON file, fetched at build time and embedded into binary |
--swagger-git-provider |
No | Git provider for swagger auth: github, gitlab, bitbucket. Auto-detects if not specified |
--grpc-port |
No | gRPC port (default: 9090) |
--http-port |
No | HTTP port (default: 8080) |
When --proto is specified, the script:
- Reads the proto file (from URL or local path) to extract service info
- Extracts service name, package, and
go_packagefrom the proto - Generates a service stub importing the proto's
go_package - Creates
main.goconfigured for the detected service (with all optional features documented) - Does not copy the proto file - you manage it externally
# From URL (proto has go_package option)
./create-service.sh --proto=https://raw.githubusercontent.com/example/api/main/service.proto \
--module=github.com/myorg/service
# From local file
./create-service.sh --proto=../shared/protos/user.proto --module=github.com/myorg/user-service
# When proto doesn't have go_package or you need to override it
./create-service.sh \
--proto=https://git.example.com/org/repo/-/raw/main/apis/v1/role.proto \
--module=github.com/myorg/role-service \
--go-package=git.example.com/org/repo/clients/v1/role-go \
--grpckit-version=v0.0.2The generated code imports the proto package directly, so ensure the generated proto code is available as a Go module (via go.mod replace directive, git submodule, or published module).
Note: If the proto file doesn't contain a go_package option, you must provide --go-package with the import path where the generated Go code is published.
Default (without --proto):
my-service/
├── main.go # Entry point with all options documented
├── internal/
│ └── service/
│ └── <name>_service.go # Full service implementation
├── proto/
│ ├── <name>.proto # Proto definition with REST annotations
│ ├── buf.yaml # Buf configuration
│ ├── buf.gen.yaml # Code generation config
│ └── gen/ # Generated code (after buf generate)
├── Makefile # Build commands
├── go.mod
└── README.md
With --proto (external proto):
my-service/
├── main.go # Entry point importing external proto package
├── internal/
│ └── service/
│ └── <name>_service.go # Service stub with TODOs
├── Makefile # Build commands
├── go.mod
└── README.md
The generated main.go has:
- Basic options enabled: Health checks, graceful shutdown
- Optional features commented out: Metrics, Swagger, CORS, auth, interceptors, etc.
- Detailed comments explaining each option
Simply uncomment the features you need.
package main
import (
"github.com/gyozatech/grpckit"
pb "your/proto/gen"
"google.golang.org/grpc"
)
func main() {
grpckit.Run(
grpckit.WithGRPCService(func(s grpc.ServiceRegistrar) {
pb.RegisterMyServiceServer(s, &MyService{})
}),
grpckit.WithRESTService(pb.RegisterMyServiceHandlerFromEndpoint),
)
}That's it! Your service is now available via:
- gRPC on port
9090 - REST on port
8080
Register multiple gRPC services from different proto files.
Note: The function wrapper
func(s grpc.ServiceRegistrar) { ... }ensures compile-time type checking between your service implementation and the generated interface.
grpckit.Run(
// Register multiple gRPC services
grpckit.WithGRPCService(func(s grpc.ServiceRegistrar) {
itempb.RegisterItemServiceServer(s, NewItemService())
}),
grpckit.WithGRPCService(func(s grpc.ServiceRegistrar) {
userpb.RegisterUserServiceServer(s, NewUserService())
}),
grpckit.WithGRPCService(func(s grpc.ServiceRegistrar) {
orderpb.RegisterOrderServiceServer(s, NewOrderService())
}),
// Register their REST handlers
grpckit.WithRESTService(itempb.RegisterItemServiceHandlerFromEndpoint),
grpckit.WithRESTService(userpb.RegisterUserServiceHandlerFromEndpoint),
grpckit.WithRESTService(orderpb.RegisterOrderServiceHandlerFromEndpoint),
// Configuration applies to all services
grpckit.WithHealthCheck(),
grpckit.WithCORS(),
)All services share:
- Same gRPC port (9090) and HTTP port (8080)
- Same interceptors and middleware
- Same authentication configuration
- Same CORS settings
grpckit.Run(
// Services
grpckit.WithGRPCService(func(s grpc.ServiceRegistrar) {
pb.RegisterMyServiceServer(s, &MyService{})
}),
grpckit.WithRESTService(pb.RegisterMyServiceHandlerFromEndpoint),
// Ports (use same port for combined gRPC + REST)
grpckit.WithGRPCPort(8080),
grpckit.WithHTTPPort(8080),
// Authentication
grpckit.WithAuth(myAuthFunc),
grpckit.WithProtectedEndpoints("/api/v1/admin/*"),
// OR
grpckit.WithPublicEndpoints("/healthz", "/readyz", "/metrics"),
// Features
grpckit.WithHealthCheck(),
grpckit.WithMetrics(),
grpckit.WithSwagger("https://example.com/api/swagger.json"), // embedded at build time via 'make swagger'
// Or use WithSwaggerFile("./api/swagger.json") to read from disk at runtime
// Graceful shutdown
grpckit.WithGracefulShutdown(30 * time.Second),
)| Variable | Description | Default |
|---|---|---|
GRPCKIT_GRPC_PORT |
gRPC server port | 9090 |
GRPCKIT_HTTP_PORT |
HTTP/REST server port | 8080 |
GRPCKIT_HEALTH_ENABLED |
Enable health endpoints | false |
GRPCKIT_METRICS_ENABLED |
Enable metrics endpoint | false |
GRPCKIT_SWAGGER_ENABLED |
Enable Swagger UI | false |
GRPCKIT_SWAGGER_PATH |
Path to swagger.json | - |
GRPCKIT_LOG_LEVEL |
Log level (debug, info, warn, error) | info |
GRPCKIT_GRACEFUL_TIMEOUT |
Shutdown timeout (e.g., "30s") | 30s |
# grpckit.yaml
grpc:
port: 9090
http:
port: 8080
health:
enabled: true
metrics:
enabled: true
swagger:
enabled: true
path: "./api/swagger.json"
auth:
protected_endpoints:
- "/api/v1/admin/*"
public_endpoints:
- "/healthz"
- "/readyz"Load with:
grpckit.Run(
grpckit.WithConfigFile("grpckit.yaml"),
// ... other options override config file
)By default, gRPC and HTTP/REST run on separate ports. To run both on the same port, simply set them to the same value:
grpckit.WithGRPCPort(8080),
grpckit.WithHTTPPort(8080),When the ports match, grpckit automatically uses HTTP/2 cleartext (h2c) multiplexing to route:
Content-Type: application/grpc→ gRPC server- Everything else → REST/HTTP handler
This is useful for:
- Simplified deployment (single port to expose)
- Kubernetes services with single port
- Load balancers that only support one backend port
authFunc := func(ctx context.Context, token string) (context.Context, error) {
if token == "" {
return nil, grpckit.ErrUnauthorized
}
// Validate token and extract user info
userID, err := validateToken(token)
if err != nil {
return nil, grpckit.ErrUnauthorized
}
// Return enriched context
return context.WithValue(ctx, "user_id", userID), nil
}// Option 1: Protect specific endpoints (allowlist)
grpckit.WithAuth(authFunc),
grpckit.WithProtectedEndpoints(
"/api/v1/users/*",
"/api/v1/admin/*",
)
// Option 2: Make everything protected except specific endpoints (denylist)
grpckit.WithAuth(authFunc),
grpckit.WithPublicEndpoints(
"/healthz",
"/readyz",
"/metrics",
"/swagger/*",
)Enable Cross-Origin Resource Sharing (CORS) to allow browser requests from different origins.
// Enable permissive CORS (allows all origins)
grpckit.WithCORS()grpckit.WithCORSConfig(grpckit.CORSConfig{
AllowedOrigins: []string{"https://example.com", "https://app.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"Authorization", "Content-Type"},
AllowCredentials: true,
MaxAge: 3600, // Cache preflight for 1 hour
})When using WithCORS(), the default configuration:
- Allows all origins (
*) - Allows common HTTP methods (GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD)
- Allows common headers (Authorization, Content-Type, etc.)
- Enables credentials (when not using wildcard origin)
- Caches preflight requests for 24 hours
grpckit supports multiple content types beyond JSON/protobuf. Enable them with simple options.
grpckit.Run(
grpckit.WithGRPCService(...),
grpckit.WithRESTService(...),
// Enable form submissions
grpckit.WithFormURLEncodedSupport(),
// Enable XML
grpckit.WithXMLSupport(),
// Enable binary data
grpckit.WithBinarySupport(),
// Enable file uploads
grpckit.WithMultipartSupport(),
// Enable plain text
grpckit.WithTextSupport(),
)| Option | Content-Type | Use Case |
|---|---|---|
WithFormURLEncodedSupport() |
application/x-www-form-urlencoded |
HTML form submissions |
WithXMLSupport() |
application/xml |
Legacy API compatibility |
WithBinarySupport() |
application/octet-stream |
File downloads, raw bytes |
WithMultipartSupport() |
multipart/form-data |
File uploads |
WithTextSupport() |
text/plain |
Plain text endpoints |
Accept HTML form submissions:
grpckit.WithFormURLEncodedSupport()curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "name=John&email=john@example.com&age=30"Field mapping:
- Uses proto field names (snake_case)
- Nested fields via dot notation:
address.street=123 - Repeated fields via multiple values:
tags=a&tags=b
grpckit.WithMultipartSupport()Define your proto with file fields:
message UploadRequest {
string description = 1;
bytes file_data = 2; // File contents
string file_name = 3; // Original filename
string file_type = 4; // Content-Type
}Upload files:
curl -X POST http://localhost:8080/api/v1/upload \
-F "description=My document" \
-F "file=@document.pdf"Configure JSON serialization behavior:
grpckit.WithJSONOptions(grpckit.JSONOptions{
UseProtoNames: true, // Use snake_case instead of camelCase
EmitUnpopulated: true, // Include fields with zero values
Indent: " ", // Pretty print
DiscardUnknown: true, // Ignore unknown fields on input
})Register your own marshaler for any content type:
grpckit.WithMarshaler("application/msgpack", &MyMsgPackMarshaler{})Or register multiple at once:
grpckit.WithMarshalers(map[string]runtime.Marshaler{
"application/msgpack": &MyMsgPackMarshaler{},
"application/yaml": &MyYAMLMarshaler{},
})- Request: Marshaler selected based on
Content-Typeheader - Response: Marshaler selected based on
Acceptheader - Fallback: JSON is used when no specific marshaler matches
Register HTTP endpoints outside of proto/gRPC. These are pure HTTP handlers that:
- Are NOT exposed via gRPC (HTTP only)
- Can use any input/output format (not constrained by proto)
- Support custom per-handler middleware
grpckit.Run(
grpckit.WithGRPCService(...),
grpckit.WithRESTService(...),
// Custom HTTP endpoint (not in proto)
grpckit.WithHTTPHandler("/webhook", webhookHandler),
grpckit.WithHTTPHandlerFunc("/upload", uploadFunc),
)Wrap handlers with dedicated middleware:
// Webhook with signature validation middleware
grpckit.WithHTTPHandler("/webhook",
webhookAuthMiddleware("secret")(
http.HandlerFunc(webhookHandler),
),
),Add middleware that applies to ALL HTTP requests:
grpckit.WithHTTPMiddleware(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("[HTTP] %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
})Request
↓
metrics middleware (built-in)
↓
auth middleware (built-in)
↓
custom global middleware(s)
↓
per-handler middleware (if wrapped)
↓
Handler
Add custom interceptors for ALL gRPC calls. Interceptors are the gRPC equivalent of HTTP middleware.
For request-response RPC calls:
grpckit.WithUnaryInterceptor(func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
log.Printf("[gRPC] %s", info.FullMethod)
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("[gRPC] %s took %v", info.FullMethod, time.Since(start))
return resp, err
})For streaming RPC calls:
grpckit.WithStreamInterceptor(func(
srv interface{},
ss grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
log.Printf("[gRPC Stream] %s", info.FullMethod)
return handler(srv, ss)
})gRPC Request
↓
auth interceptor (built-in, if configured)
↓
custom interceptor 1 (first WithUnaryInterceptor call)
↓
custom interceptor 2 (second WithUnaryInterceptor call)
↓
... more custom interceptors ...
↓
Handler
- Logging: Log method calls, durations, errors
- Metrics: Track request counts, latencies
- Tracing: Add distributed tracing spans
- Validation: Validate requests before handlers
- Recovery: Catch panics and convert to errors
Skip specific endpoints from an interceptor using ExceptEndpoints:
// Timing interceptor that skips high-frequency endpoints
grpckit.WithUnaryInterceptor(timingInterceptor,
grpckit.ExceptEndpoints(
"/item.v1.ItemService/HealthCheck",
"/item.v1.ItemService/ListItems",
),
)
// Same for stream interceptors
grpckit.WithStreamInterceptor(streamInterceptor,
grpckit.ExceptEndpoints("/item.v1.ItemService/StreamItems"),
)Endpoints should be in the format /package.Service/Method.
| Endpoint | Description | Option |
|---|---|---|
/healthz |
Liveness probe (always returns 200 if running) | WithHealthCheck() |
/readyz |
Readiness probe (returns 503 if not ready) | WithHealthCheck() |
/metrics |
Prometheus metrics | WithMetrics() |
/swagger/ |
Swagger UI | WithSwagger(url) or WithSwaggerFile(path) |
/swagger/spec.json |
OpenAPI spec | WithSwagger(url) or WithSwaggerFile(path) |
grpckit provides common errors for use in your services:
grpckit.ErrUnauthorized // 401 - Missing or invalid token
grpckit.ErrForbidden // 403 - Insufficient permissions
grpckit.ErrNotFound // 404 - Resource not found
grpckit.ErrInvalidConfig // Invalid configuration
grpckit.ErrServiceNotRegistered // No services registeredserver, err := grpckit.New(
grpckit.WithGRPCService(...),
grpckit.WithRESTService(...),
)
if err != nil {
log.Fatal(err)
}
// Access gRPC server for advanced configuration
grpcServer := server.GRPCServer()
// Control readiness
server.SetReady(false) // Mark as not ready
server.SetReady(true) // Mark as ready
// Start the server
server.Start()grpckit provides test utilities for in-memory testing without network ports.
TestServer wraps the grpckit server with in-memory connections using bufconn for gRPC and httptest for REST.
func TestMyService(t *testing.T) {
// Create test server (no real ports needed)
ts, err := grpckit.NewTestServer(
grpckit.WithGRPCService(func(s grpc.ServiceRegistrar) {
pb.RegisterMyServiceServer(s, NewMyService())
}),
grpckit.WithRESTService(pb.RegisterMyServiceHandlerFromEndpoint),
grpckit.WithHealthCheck(),
)
if err != nil {
t.Fatal(err)
}
defer ts.Close()
// Test gRPC
conn := ts.GRPCClientConn(context.Background())
client := pb.NewMyServiceClient(conn)
resp, err := client.GetItem(ctx, &pb.GetItemRequest{Id: "123"})
// Test REST
httpResp, err := ts.HTTPClient().Get(ts.URL("/api/v1/items/123"))
// Control readiness during tests
ts.SetReady(false)
// ... test behavior when not ready ...
ts.SetReady(true)
}| Method | Description |
|---|---|
GRPCClientConn(ctx) |
Returns a *grpc.ClientConn for in-memory gRPC calls |
HTTPClient() |
Returns an *http.Client configured for the test server |
BaseURL() |
Returns the base URL (e.g., http://127.0.0.1:12345) |
URL(path) |
Constructs full URL for a path (e.g., ts.URL("/api/v1/items")) |
SetReady(bool) |
Controls the readiness state |
Close() |
Shuts down the test server |
Easily configure authentication for tests:
// Accept a single token
grpckit.WithAuth(grpckit.MockAuthFunc("valid-token", "user-123"))
// Accept multiple tokens with different user IDs
grpckit.WithAuth(grpckit.MockAuthFuncMultiple(map[string]string{
"admin-token": "admin-user",
"user-token": "regular-user",
}))
// Accept any token (disable auth checks)
grpckit.WithAuth(grpckit.MockAuthFuncAllowAll())func TestItemService_CRUD(t *testing.T) {
ts, _ := grpckit.NewTestServer(
grpckit.WithGRPCService(func(s grpc.ServiceRegistrar) {
pb.RegisterItemServiceServer(s, NewItemService())
}),
grpckit.WithRESTService(pb.RegisterItemServiceHandlerFromEndpoint),
grpckit.WithAuth(grpckit.MockAuthFunc("test-token", "test-user")),
grpckit.WithPublicEndpoints("/api/v1/items"), // List is public
)
defer ts.Close()
// Test via gRPC
client := pb.NewItemServiceClient(ts.GRPCClientConn(context.Background()))
createResp, err := client.CreateItem(ctx, &pb.CreateItemRequest{
Name: "Test Item",
})
if err != nil {
t.Fatalf("CreateItem failed: %v", err)
}
// Test via REST
resp, _ := ts.HTTPClient().Get(ts.URL("/api/v1/items"))
// ... assert response ...
}See the example directory for a complete working example with:
- Proto definitions with REST annotations
- CRUD service implementation
- Authentication
- All features enabled
Apache 2.0
