Micronaut API gateway in action

In this post, I am going to show how we can implement a simple gateway service for the Maps microservice developed in the earlier post. Gateway microservice is another Micronaut service that consumes our Maps service and provides public API access for users, requests are routed using gateway which can be managed, monitored, secured and metered.

We will be using Consul for service discovery, Micronaut has built-in support for Consul. Each of our Micronaut service registers with Consul on startup. We will also setup Swagger docs for our APIs, which allows us to test APIs using Swagger UI.

Generate a Micronaut app for our API gateway service using MN CLI command, which enables configuration for Consul.

mn create-app --features http-client,spock,discovery-consul

It creates a Micronaut configuration that includes configuration for consul.

---
micronaut:
    application:
        name: maps-gateway

---
consul:
  client:
    registration:
      enabled: true
    defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"

MapClient is annotated using Micronaut’s declarative HTTP client which references our backend service using its name defined in the Micronaut configuration.

import io.micronaut.http.client.annotation.Client;

@Client(id = "maps-service", path = "/maps")
public interface MapClient extends MapOperations {
}

API methods are abstracted in MapOperations interface.

import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.QueryValue;
import io.reactivex.Single;

import maps.common.Compares;
import maps.common.Direction;
import maps.common.Directions;
import maps.common.MapProvider;

import javax.validation.constraints.NotBlank;
import java.util.List;

public interface MapOperations {
    @Get("/{provider}")
    Single<List<Direction>> map(@NotBlank MapProvider provider, @NotBlank @QueryValue String src, @NotBlank @QueryValue String dest);

    @Get("/compare")
    Single<Compares> compare(@NotBlank @QueryValue String src, @NotBlank @QueryValue String dest);

    @Get("/shortest")
    Single<Directions> shortest(@NotBlank @QueryValue String src, @NotBlank @QueryValue String dest);

    @Get("/fastest")
    Single<Directions> fastest(@NotBlank @QueryValue String src, @NotBlank @QueryValue String dest);
}

Then our GatewayController delegates requests to the MapClient.

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.QueryValue;
import io.reactivex.Single;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;

import maps.common.Direction;
import maps.common.Directions;
import maps.common.MapProvider;

import javax.validation.constraints.NotBlank;
import java.util.List;

@Controller("/api/maps")
public class GatewayController implements MapOperations {

    private final MapClient mapClient;

    public GatewayController(MapClient mapClient) {
        this.mapClient = mapClient;
    }

    @Get("/{provider}")
    @Operation(summary = "Find directions",
            tags = {"maps"},
            responses = {
                    @ApiResponse(description = "", content = @Content(mediaType = "application/json",
                            schema = @Schema(implementation = List.class)))}
    )
    public Single<List<Direction>> map(@NotBlank MapProvider provider, @NotBlank @QueryValue String src, @NotBlank @QueryValue String dest) {
        return mapClient.map(provider, src, dest);
    }

    @Get("/shortest")
    @Operation(summary = "Find shortest route",
            tags = {"maps"},
            responses = {
                    @ApiResponse(description = "", content = @Content(mediaType = "application/json",
                            schema = @Schema(implementation = Directions.class)))}
    )
    public Single<Directions> shortest(@NotBlank @QueryValue String src, @NotBlank @QueryValue String dest) {
        return mapClient.shortest(src, dest);
    }

    @Get("/fastest")
    @Operation(summary = "Find fastest route",
            tags = {"maps"},
            responses = {
                    @ApiResponse(description = "", content = @Content(mediaType = "application/json",
                            schema = @Schema(implementation = Directions.class)))}
    )
    public Single<Directions> fastest(@NotBlank @QueryValue String src, @NotBlank @QueryValue String dest) {
        return mapClient.fastest(src, dest);
    }
}

You can see the controller methods annotated with Swagger APIs, which allows us to generate documentation for our APIs and host it within our gateway application.

Micronaut Gateway service is all set and configured with Consul and we can start Consul and multiple instances of our backend Map service.

Our Gateway service will be running on the default Micronaut server port 8080, while I have configured the backend service to use random ports, so any number of backend microservice can be run. Here is the Micronaut configuration for our backend microservice.

---
micronaut:
    application:
        name: maps-service
    server:
        port: -1

---
consul:
  client:
    registration:
      enabled: true
    defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"

First, start consul, using Docker:

docker run -p 8500:8500 consul:1.4.4

Navigate to maps-gateway folder and run: .\gradlew run

Navigate to maps-service folder and run: .\gradlew run
Run 3 instances of maps backend microservice, so we can see the client side load balancing in action.

Looking at Consul web interface, should list all your services (1 gateway and 3 services).

Our APIs are ready to be consumed from the gateway microservice. Using curl, let’s invoke the API to get directions from google to apple using Apple Maps provider.

curl "http://localhost:8080/api/maps/apple?src=google&dest=apple"
[
  {
    "order": 0,
    "text": "Turn left to merge onto I-280 North",
    "distanceInMiles": 0.2,
    "timeInMinutes": 1
  },
  {
    "order": 1,
    "text": "Take exit 12A to merge onto CA-85 North toward Mtn View",
    "distanceInMiles": 1,
    "timeInMinutes": 1
  },
  {
    "order": 2,
    "text": "Take exit 24B on the left to merge onto US-101 North toward San Francisco",
    "distanceInMiles": 5.5,
    "timeInMinutes": 6
  },
  {
    "order": 3,
    "text": "Take exit 400A toward Amphitheater Parkway",
    "distanceInMiles": 1.6,
    "timeInMinutes": 1
  },
  {
    "order": 4,
    "text": "Take right onto Charleston Rd",
    "distanceInMiles": 0.3,
    "timeInMinutes": 1
  },
  {
    "order": 5,
    "text": "Turn left",
    "distanceInMiles": 0.4,
    "timeInMinutes": 1
  }
]

Then, the next API to find the fastest route from apple to google:

curl "http://localhost:8080/api/maps/fastest?src=apple&dest=google" 
{
  "directions": [
    {
      "order": 0,
      "text": "Turn right onto N Shoreline Blvd",
      "distanceInMiles": 0.3,
      "timeInMinutes": 1
    },
    {
      "order": 1,
      "text": "Take a slight right turn to merge onto CA-85 South",
      "distanceInMiles": 0.6,
      "timeInMinutes": 1
    },
    {
      "order": 2,
      "text": "Keep left to merge onto CA-85 South",
      "distanceInMiles": 0.2,
      "timeInMinutes": 1
    },
    {
      "order": 3,
      "text": "Take the exit to merge onto I-280 toward San Jose",
      "distanceInMiles": 5.4,
      "timeInMinutes": 6
    },
    {
      "order": 4,
      "text": "Take exit 11 onto De Anza Boulevard",
      "distanceInMiles": 1.5,
      "timeInMinutes": 1
    },
    {
      "order": 5,
      "text": "Turn right onto N De Anza Blvd toward Cupertino, Saratoga",
      "distanceInMiles": 0.2,
      "timeInMinutes": 1
    },
    {
      "order": 6,
      "text": "Turn left onto Mariani Ave",
      "distanceInMiles": 0.3,
      "timeInMinutes": 1
    }
  ],
  "timeInMinutes": 12
}

Client side load balancing ensures requests are routed to backend services in a round robin fashion by default. Other load balancing strategies can be implemented using Netflix Ribbon.

We have the basic infrastructure in place for our Maps service, the next thing we could do is enhance our sample application by adding additional capabilities such as monitoring. I will be exploring that in the next part of this series.

Source code for the same application is available in GitHub.

One thought on “Micronaut API gateway in action

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s