Go & Kong: Building a Secure API Gateway for Microservices

Learn how to use Kong as an API gateway for Go microservices with JWT authentication, rate limiting, and gRPC internal communication.

George Lopez Dec 1st, 2025
Kong Go Architecture

In this article, we show how to use Kong, a lightweight and scalable API gateway, to manage traffic between microservices written in Go. This demonstration has two services, a Users API and a Payments API. All external traffic is routed through Kong with JWT authentication and rate limiting. We will also demonstrate how the services communicate internally via gRPC while keeping the public surface secure and predictable.

Link to the GitHub repo here.

What is Kong?

Kong is an open-source API gateway that sits in front of your services to handle routing, authentication, rate limiting, and observability. In a microservice architecture, it acts as a protective layer between external clients and your internal services, freeing individual services from concern about security and traffic control. By centralising these concerns in Kong, each microservice can focus purely on business logic. Kong handles authenticating requests, throttling traffic, and directing calls to the right upstream service.

Kong Configuration

Kong can be set up in two ways: with a database (DB mode) or without one (DB-less mode). In DB mode, Kong stores its configuration in PostgreSQL or Cassandra and provides a RESTful Admin API for dynamic updates. In DB-less mode, the entire gateway state is defined in a single declarative file and loaded at startup. For this project, we chose DB-less mode. There is no database to manage, which keeps the local setup simple. The entire gateway is defined in _kong/kong.yaml. This file declares our two upstream services the users-api and payments-api along with their routes, plugins, and consumers.

services:
  - name: payments-api
    url: http://payments-api:8080
    ...
  - name: users-api
    url: http://users-api:8080
    ...

Kong - Endpoints & Routing

Routes define how Kong directs incoming requests to the correct service. In our configuration, this can be handled at the route level or more globally at the service level. For the Users API, we have two routes on the same path. The users-create-route matches POST /api/v1/users and does not require authentication, to mimic account signup. The users-route matches the same path for all other methods and requires authentication.

routes:
  - name: users-create-route
    paths:
      - /api/v1/users
    methods:
      - POST
    strip_path: false
  - name: users-route
    paths:
      - /api/v1/users
    strip_path: false

For the Payments API, we take a more global approach. The JWT plugin is applied at the service level, so every route under payments-api inherits it automatically.

services:
  - name: payments-api
    url: http://payments-api:8080
    plugins:
      - name: jwt
        ...
    routes:
      - name: payments-route
        paths:
          - /api/v1/payments
          - ~/api/v1/users/([^/]+)/payments
        strip_path: false

This flexibility lets you enforce policies broadly or carve out exceptions for specific routes.

Kong - Rate Limiting

Rate limiting protects your services from spam and accidental overload. By capping the number of requests a client can make in a given window, you ensure fair usage and prevent any single source from degrading performance for everyone else. In our kong.yaml, rate limiting is configured per service under each service’s plugins section.

Payments API — 50 per minute, 500 per hour, 2,000 per day:

- name: rate-limiting
  config:
    minute: 50
    hour: 500
    day: 2000
    policy: local
    limit_by: ip

Users API — 100 per minute, 1,000 per hour, 5,000 per day:

- name: rate-limiting
  config:
    minute: 100
    hour: 1000
    day: 5000
    policy: local
    limit_by: ip

The local policy stores counters in memory, which is sufficient for a single-node local setup. Later, we will validate these limits with a K6 load test.

Kong - JWT Authentication

Authentication ensures that only known clients can access protected resources. In our setup, JWT is applied selectively. The POST /api/v1/users route remains open so new clients can register. All other routes require a valid JWT signed with the configured secret. The JWT plugin is configured in kong.yaml:

plugins:
  - name: jwt
    config:
      uri_param_names: []
      cookie_names: []
      key_claim_name: iss
      secret_is_base64: false
      claims_to_verify:
        - exp
      maximum_expiration: 86400

At the heart of this setup is a consumer. In Kong, a consumer represents a client or application that can consume your APIs. We define a single consumer with a JWT secret:

consumers:
  - username: $JWT_CONSUMER_USERNAME
    jwt_secrets:
      - key: $JWT_KEY
        secret: $JWT_SECRET
        algorithm: HS256

NOTE: This is a simplified setup for demonstration purposes. It does not adhere to proper authorization standards — the JWT is used for authentication only, and the user ID is neither verified nor embedded in the token claims.

Cross-Microservice Communication

Our two services can communicate internally through gRPC. When we want to create a payment for a user, the Payments API fetches the user from the Users API over gRPC to confirm they exist and are over 18 before accepting the payment. This avoids an internal HTTP call, and gives lower latency and stricter contracts. The users.proto file defines a single RPC method:

service UserService {
  rpc GetUserByID (GetUserByIDRequest) returns (GetUserByIDResponse);
}

The Users API registers a gRPC server alongside its HTTP server:

server := http.NewServer("8080", services)
go server.Start()
 
grpcServer := grpc.NewServer(os.Getenv("GRPC_PORT"), services)
grpcServer.Start()

The Payments API dials the Users API over an internal address:

userGRPC := grpc.NewUserGRPC(os.Getenv("USERS_API_GRPC_ADDR"))

This pattern keeps service-to-service communication fast and type-safe, while Kong manages all inbound traffic from the outside world.

Load Testing - K6

We use load testing to ensure the rate limiting we configured is actually being applied. A K6 script in _k6/load.js sends repeated POST requests to the public POST /api/v1/users endpoint through Kong. The test runs 10 virtual users for 10 seconds, intentionally exceeding the 100 requests-per-minute limit on the Users API.

make run-load
K6 Load Testing Results

The results highlight Kong’s rate limiting in action. Requests are either accepted (201 Created) or rejected with 429 Too Many Requests once the limit is exceeded, confirming the gateway is protecting the upstream service as intended.

End To End Testing - Hurl

For a quick, easy end-to-end test we used Hurl. The test file in _hurl/flow.hurl exercises the entire system. Creating users, authenticating via JWT, making payments, and verifying business rules. It defines two scenarios. First, an adult user (> 18) is created, retrieved, and used to make a valid payment:

POST {{BASE_URL}}/api/v1/users
Content-Type: application/json
{
  "name": "Alice",
  "age": 25
}
HTTP 201

Then a child user (< 18) is created, and a payment attempt is correctly rejected:

POST {{BASE_URL}}/api/v1/users/{{child_user_id}}/payments
Authorization: Bearer {{JWT_TOKEN}}
Content-Type: application/json
{
  "amount": 50
}
HTTP 400

You can run this yourself with the following command:

make run-hurl

This gives us confidence that Kong, the microservices, and the business logic all work together correctly.

Conclusion

In this article, we showed how Kong can serve as a centralised API gateway for Go microservices, handling routing, JWT authentication, and rate limiting through a declarative configuration. By keeping the gateway stateless and DB-less, the setup remains simple to run locally while mirroring patterns that scale to production. Combined with gRPC for internal service communication, K6 for load validation, and Hurl for end-to-end testing, this architecture provides a solid foundation for building secure, observable, and performant APIs in Go.

Go Kong Gopher