A gRPC-Gateway-powered framework with Prisma, Kubernetes, and Go for scalable microservices.
βοΈ gRPC + REST (gRPC-Gateway) β Automatically expose RESTful APIs from gRPC services.
βοΈ Prisma Integration β Use Prisma for efficient database access in Go.
βοΈ Kubernetes Ready β Easily deploy and scale with Kubernetes.
βοΈ TLS Security β Secure gRPC communications with TLS.
βοΈ Structured Logging β Built-in zap
logging.
βοΈ Authentication β Secure endpoints with built-in authentication mechanisms.
βοΈ Rate Limiting & Authentication β Pre-configured middleware.
βοΈ Modular & Extensible β Easily extend Thunder for custom use cases.
Ensure you have Go, protoc
, and Prisma installed.
go mod tidy
Create a .proto
file, e.g., user.proto
:
syntax = "proto3";
package example;
option go_package = "backend/";
import "google/api/annotations.proto";
// A simple service definition.
service UserService {
rpc GetUser (UserRequest) returns (UserResponse) {
option (google.api.http) = {
get: "/v1/users/{id}"
};
}
}
// Request and response messages.
message UserRequest {
int32 id = 1;
}
message UserResponse {
int32 id = 1;
string name = 2;
int32 age = 3;
}
Thunder automatically integrates Prisma for database management. Define your schema:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @default(cuid()) @id
name String
email String @unique
}
To generate your gRPC server implementations, you can build a custom protoc plugin. In Thunder, the plugin is built as follows:
go build -o protoc-gen-rpc-impl ./cmd/protoc-gen-rpc-impl.go
sudo mv protoc-gen-rpc-impl /usr/local/bin
sudo chmod +x /usr/local/bin/protoc-gen-rpc-impl
go run generator.go -proto=filename.proto -prisma=true
Note: Replace
filename
with the actual name of your gRPC service.
go run ./server/main.go
Note: Generate TLS certificates prior running the server.
To mock a gRPC server:
cd backend
mockgen -source=yourservice_grpc.pb.go -destination=yourservice_mock.go
Note: Replace
yourservice
with the actual name of your gRPC service. Look into /backend/authenticator_server_test.go to see how to develop tests or look into https://github.com/golang/mock
mkdir certs
openssl req -x509 -newkey rsa:4096 -keyout certs/server.key -out certs/server.crt -days 365 -nodes \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
docker build -t app:latest .
docker login
docker push $docker_username/app:latest
Note: Edit
k8s/app-deployment.yaml
before deploying.
minikube start
cd k8s
kubectl apply -f postgres-deployment.yaml
kubectl apply -f postgres-service.yaml
kubectl apply -f postgres-pvc.yaml
kubectl apply -f app-deployment.yaml
kubectl apply -f app-service.yaml
kubectl apply -f pgbouncer-all.yaml
kubectl rollout restart deployment pgbouncer
kubectl rollout restart deployment app-deployment
kubectl port-forward service/app-service 8080:8080
kubectl get pods -n default
kubectl describe pod $NAME -n default
curl -k --http2 -X POST https://localhost:8080/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "password123",
"name": "John",
"surname": "Doe",
"age": 30
}'
curl -k --http2 -X POST https://localhost:8080/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "password123"
}'
Example of a gRPC server running with authentication and rate limiting:
package main
import (
"context"
"db"
"io"
"log"
"net"
"net/http"
pb "backend"
"middlewares"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/opentracing/opentracing-go"
"github.com/spf13/viper"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/config"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
)
// initConfig sets default values and loads environment variables.
func initConfig() {
viper.SetDefault("grpc.port", ":50051")
viper.SetDefault("http.port", ":8080")
viper.AutomaticEnv()
}
// initJaeger initializes a Jaeger tracer based on environment configuration.
func initJaeger(service string) (opentracing.Tracer, io.Closer) {
cfg, err := config.FromEnv()
if err != nil {
log.Fatalf("Failed to read Jaeger env vars: %v", err)
}
cfg.ServiceName = service
tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger))
if err != nil {
log.Fatalf("Could not initialize Jaeger tracer: %v", err)
}
opentracing.SetGlobalTracer(tracer)
return tracer, closer
}
// RegisterServers registers gRPC servers.
func RegisterServers(server *grpc.Server, client *db.PrismaClient, sugar *zap.SugaredLogger) {
pb.RegisterAuthServer(server, &pb.AuthenticatorServer{
PrismaClient: client,
Logger: sugar,
})
}
// RegisterHandlers registers gRPC-Gateway handlers.
func RegisterHandlers(gwmux *runtime.ServeMux, conn *grpc.ClientConn) {
err := pb.RegisterAuthHandler(context.Background(), gwmux, conn)
if err != nil {
log.Fatalln("Failed to register gateway:", err)
}
}
func main() {
// Initialize configuration.
initConfig()
// Setup structured logging.
logger, err := zap.NewProduction()
if err != nil {
log.Fatal(err)
}
sugar := logger.Sugar()
defer logger.Sync()
// Initialize Jaeger tracer.
_, closer := initJaeger("thunder-grpc")
defer closer.Close()
// Load TLS credentials for the gRPC server.
certFile := "../certs/server.crt"
keyFile := "../certs/server.key"
creds, err := credentials.NewServerTLSFromFile(certFile, keyFile)
if err != nil {
sugar.Fatalf("Failed to load TLS credentials: %v", err)
}
// Listen on the configured gRPC port.
grpcPort := viper.GetString("grpc.port")
lis, err := net.Listen("tcp", grpcPort)
if err != nil {
log.Fatalln("Failed to listen:", err)
}
// Connect to the database.
client := db.NewClient()
if err := client.Prisma.Connect(); err != nil {
panic(err)
}
defer func() {
if err := client.Prisma.Disconnect(); err != nil {
panic(err)
}
}()
// Initialize rate limiter (e.g., 5 requests per second, burst of 10).
rateLimiter := middlewares.NewRateLimiter(5, 10)
// Create the gRPC server with TLS and custom interceptors.
grpcServer := grpc.NewServer(
grpc.Creds(creds),
grpc.UnaryInterceptor(
middlewares.ChainUnaryInterceptors(
rateLimiter.RateLimiterInterceptor, // Rate limiting
middlewares.AuthUnaryInterceptor, // Authentication
),
),
)
RegisterServers(grpcServer, client, sugar)
// Register gRPC Health service.
healthServer := health.NewServer()
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
sugar.Infof("Serving gRPC with TLS on 0.0.0.0%s", grpcPort)
go func() {
log.Fatalln(grpcServer.Serve(lis))
}()
// Setup secure connection for gRPC-Gateway.
clientCreds, err := credentials.NewClientTLSFromFile(certFile, "localhost")
if err != nil {
sugar.Fatalf("Failed to load client TLS credentials: %v", err)
}
conn, err := grpc.Dial(
"localhost"+grpcPort,
grpc.WithTransportCredentials(clientCreds),
)
if err != nil {
log.Fatalln("Failed to dial gRPC server:", err)
}
// Register gRPC-Gateway handlers.
gwmux := runtime.NewServeMux()
RegisterHandlers(gwmux, conn)
// Create a new HTTP mux and add health and readiness endpoints.
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
// You can add more logic here if needed.
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
mux.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) {
// You might include checks (e.g., database connectivity) before reporting readiness.
w.WriteHeader(http.StatusOK)
w.Write([]byte("Ready"))
})
mux.Handle("/", gwmux)
httpPort := viper.GetString("http.port")
gwServer := &http.Server{
Addr: httpPort,
Handler: mux,
}
sugar.Infof("Serving gRPC-Gateway on https://0.0.0.0%s", httpPort)
log.Fatalln(gwServer.ListenAndServeTLS("../certs/server.crt", "../certs/server.key"))
}
A simple gRPC client:
package main
import (
. "backend"
"context"
"crypto/tls"
"fmt"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
)
func main() {
// Create a custom TLS config that skips certificate verification.
tlsConfig := &tls.Config{
InsecureSkipVerify: true, // Only use this for testing!
}
tlsCreds := credentials.NewTLS(tlsConfig)
// Dial the gRPC server using TLS credentials.
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(tlsCreds), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := NewAuthClient(conn)
ctx := context.Background()
registerReply, err := client.Register(ctx, &RegisterRequest{
Email: "[email protected]",
Password: "password",
Name: "Kamil",
Surname: "Mosciszko",
Age: 27,
})
if err != nil {
panic(err)
}
fmt.Println("Received registration response:", registerReply)
loginReply, err := client.Login(ctx, &LoginRequest{
Email: "[email protected]",
Password: "password",
})
if err != nil {
log.Fatalf("Login failed: %v", err)
}
token := loginReply.Token
fmt.Println("Received JWT token:", token)
md := metadata.Pairs("authorization", token)
outgoingCtx := metadata.NewOutgoingContext(ctx, md)
protectedReply, err := client.SampleProtected(outgoingCtx, &ProtectedRequest{
Text: "Hello from client",
})
if err != nil {
log.Fatalf("SampleProtected failed: %v", err)
}
fmt.Println("SampleProtected response:", protectedReply.Result)
}
Want to improve Thunder? π
- Fork the repo
- Create a feature branch (
git checkout -b feature-new
) - Commit your changes (
git commit -m "Added feature"
) - Push to your branch (
git push origin feature-new
) - Submit a PR!
- π Go Documentation
- π gRPC-Gateway
- π οΈ Prisma ORM
- βοΈ Kubernetes Docs
β Star the repo if you find it useful!
π§ For questions, reach out via GitHub Issues.