Mar 16, 2025
Photo by Resource Database on Unsplash
While working in software engineering or preparing for a job interview you often would have come across a service called API Gateway. You might also be knowing at a very high level that it is the very first service that your request lands on and from there it is routed correctly to your microservices.
To give you a more formal introduction:
Let’s understand why we even need an API Gateway, that way we will get a better understanding of their importance in the micro service architecture. But first let’s also understand what is a microservice architecture in the first place.
Think of microservice architecture as a practice where we split our application say, instagram in two or more service. Each service is assigned a unique role for e.g. one service can take care of all the user related queries (such as user management—sign up / log in) and the other service can take care of handling the posts (fetching and saving it in the DB).
Let’s take a concrete idea and build upon it. Say we want to build a simple news feed service, that consists of the following microservices:
Take the following diagram into consideration:
High level overview of gateway
Let’s break it down, we have our client that wants to talk to the Post Service. In order to talk to the Post Service, it needs to know the name of the service and the domain of the API Gateway. The domain of our gateway is, say, api.example.com.
The client first calls the DNS server to resolve api.example.com and gets some IP address. This part has been omitted for simplicity. Once the client knows the IP address, it has to follow a strict convention followed by the org.
In our case we have followed the following REST notation.
api.example.com/service/:service/:path
Where :service represents the name of the microservice uniquely identified inside the vast pool of services, and :path represents the API endpoint exposed by that service.
Let’s take an example to understand, say we want to get all the post for a user whose username is kishan. First our post service should expose an API to get all the post. Say it has exposed an API.
GET /users/:userId/all → this returns all the post associated with user having userId. Since we want to fetch all the post belonging to user kishan. We need to hit, GET /users/kishan/allto the post service. But since Post service can’t be hit directly as it is present inside a private network and not exposed to the internet, we’ll have to come via the API gateway.
Here is an example of what the request might look like.
api.example.com/services/post-service/users/kishan/allLet’s break it down, when calling the above request, it will first go to the API gateway since it has the public domain of gateway. After reaching there, the gateway will analyse the request, it will break the endpoint into which service does this request was meant for, in our case it is meant for post-service. After that it will analyse to which endpoint of the post service do we need to reach to in our case it is /users/kishan/all.
The API gateway will check for post-service in its service discovery and figure out the host and port for it and initiate an HTTP request to that service. The service in turns return the response to the gateway which is then forwarded to the client.
Note, in order for client to reach to the post service, it need to know two key important part:
Here the API GW acts as a router between the client and the end application.
Since we have understood the basic terms, we will keep on explaining the process as and where required.
For this project, we will be using Vert.x as our toolkit to build our API gateway. I have written a brief introduction on Vert.x that you can follow.
Vert.x is called a "Polyglot" toolkit because it supports multiple programming languages and paradigms
To summarise it, think of it as a set of libraries that helps the developer build reactive applications that run on JVM. It uses an event-loop, a thread that runs in an infinite loop, checking for and dispatching events on the appropriate handlers. Think of an event as a network IO, file IO, timers, messages, etc., whereas handlers are the pieces that are registered by the application to handle those specific events. For e.g., if the request succeeds (an event) execute a set of instructions.
All this code goes in what is called Verticles in Vert.x, they are the basic units of deployment and execution. We’ll understand more about it as we explain it using an example in later section of this article.
Let’s start with setting up our project, we are going to use IntelliJ IDEA as our IDE.
IntelliJ New Project
This will create a new maven project for you. Since we are going to be using vertx, we’d need to import few of its dependencies in our pom.xml file.
You can fork the complete repository from my github profile: https://github.com/confusedconsciousness/gateway
We’ll be needing the following dependencies:
Here is what your pom.xml file will look like:
1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5 <modelVersion>4.0.0</modelVersion>
6
7 <groupId>org.example</groupId>
8 <artifactId>gateway</artifactId>
9 <version>1.0-SNAPSHOT</version>
10
11 <properties>
12 <maven.compiler.source>23</maven.compiler.source>
13 <maven.compiler.target>23</maven.compiler.target>
14 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
15 <vertx.version>4.5.9</vertx.version>
16 <lombok.version>1.18.30</lombok.version>
17 </properties>
18
19 <dependencies>
20 <dependency>
21 <groupId>io.vertx</groupId>
22 <artifactId>vertx-core</artifactId>
23 <version>${vertx.version}</version>
24 </dependency>
25 <dependency>
26 <groupId>io.vertx</groupId>
27 <artifactId>vertx-web</artifactId>
28 <version>${vertx.version}</version>
29 </dependency>
30 <dependency>
31 <groupId>io.vertx</groupId>
32 <artifactId>vertx-web-client</artifactId>
33 <version>${vertx.version}</version>
34 </dependency>
35 <dependency>
36 <groupId>org.projectlombok</groupId>
37 <artifactId>lombok</artifactId>
38 <version>${lombok.version}</version>
39 <scope>provided</scope>
40 </dependency>
41 <dependency>
42 <groupId>org.slf4j</groupId>
43 <artifactId>slf4j-api</artifactId>
44 <version>2.0.16</version>
45 </dependency>
46 <dependency>
47 <groupId>org.slf4j</groupId>
48 <artifactId>slf4j-simple</artifactId>
49 <version>2.0.16</version>
50 </dependency>
51 <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
52 <dependency>
53 <groupId>com.google.guava</groupId>
54 <artifactId>guava</artifactId>
55 <version>33.4.0-jre</version>
56 </dependency>
57 </dependencies>
58
59</project>
Now, let’s start with setting up our basic gateway. We’ll create a class and name it Server.java as this will be our entry point.
I have tried to make things as verbose as possible and have left comment at all places so that it is easy for a beginner to understand what a piece of code snippet is doing. Below is the code for our Server class.
You can see that this class has our main() which is the entry point for executing a program. This method is basically initiating our Server class and invoking the start() method.
In this class, we are initialising the vertx object, that will be used for deploying verticles and routers.
Vertx vertx = Vertx.vertx();Since the gateway, needs to know what all services are present, we have created what is called a ServerConfig class. This class basically stores:
1package org.example;
2
3import io.vertx.core.Vertx;
4import lombok.NoArgsConstructor;
5import lombok.extern.slf4j.Slf4j;
6import org.example.model.config.ServerConfig;
7import org.example.model.config.VertxConfig;
8import org.example.model.config.MicroserviceConfig;
9import org.example.model.endpoint.StaticEndpoint;
10import org.example.verticle.GatewayVerticle;
11import org.example.microservice.Photogram;
12import org.example.microservice.UserService;
13
14import java.util.Map;
15
16@Slf4j
17@NoArgsConstructor
18public class Server {
19
20 public static void main(String[] args) {
21 new Server().start();
22 }
23
24 public void start() {
25 log.info("Starting server");
26 // starting point of our server, this method will initialise all the components
27 Vertx vertx = Vertx.vertx();
28 // for simplicity let's add the config manually, ideally this should come from some config store
29 // we are onboarding only two services named photogram (another internal service running on localhost:8921),
30 // and user-service that is running on 8945
31 ServerConfig config = ServerConfig.builder()
32 .vertxConfig(VertxConfig.builder().defaultPort(8080).build())
33 .serviceConfigs(Map.of("photogram", MicroserviceConfig.builder()
34 .endpoint(StaticEndpoint.builder()
35 .host("localhost")
36 .port(8921)
37 .build())
38 .build(), "user-service", MicroserviceConfig.builder()
39 .endpoint(StaticEndpoint.builder()
40 .host("localhost")
41 .port(8945)
42 .build())
43 .build()))
44 .build();
45
46 // deploy our units
47 deployGatewayVerticle(vertx, config);
48 deployMicroservices(vertx, config);
49 }
50
51 private void deployGatewayVerticle(Vertx vertx, ServerConfig config) {
52 vertx.deployVerticle(new GatewayVerticle(config), res -> {
53 if (res.succeeded()) {
54 log.info("Gateway Verticle deployed");
55 } else {
56 log.error("Gateway Verticle deploy failed", res.cause());
57 }
58 });
59 }
60
61 private void deployMicroservices(Vertx vertx, ServerConfig config) {
62 // deploy the microservices as well in the same project for simplicity
63 // our photogram service is responsible for handling posts
64 vertx.deployVerticle(new Photogram(config), res -> {
65 if (res.succeeded()) {
66 log.info("Photogram Service deployed");
67 } else {
68 log.error("Photogram Service deploy failed", res.cause());
69 }
70 });
71 // our user service is responsible for managing users
72 vertx.deployVerticle(new UserService(config), res -> {
73 if (res.succeeded()) {
74 log.info("User Service deployed");
75 } else {
76 log.error("User Service deploy failed", res.cause());
77 }
78 });
79 }
80}
81
The service config contains the details such as the name of the service onboarded onto gateway and how to discover it. Whether we need to discover it via simple host and port or via some service discovery that makes use of zookeeper.
In our case, we are only dealing with two services:
Notice both of the above services are running locally, one is running at localhost:8921 and the other is running at localhost:8945 .
You can change the ports if you get any error while bringing up the gateway; it might be possible that those ports mentioned above are used by some other application. Or, you can also create more services and try it on your own.
After defining our services, it is now time to deploy our gateway and our microservices as well. For that, we have created two methods:
From the name, it is pretty evident what they do. If you look closely, they all are very similar. Both of these methods use the deployVerticle() method present in the vertx object. The method takes a Verticle object and a handler that is invoked if the verticle is deployed successfully or not.
Let’s understand what a verticle is before going forward in detail.
Verticles are chunks of code that get deployed and run by Vert.x. Here, we are deploying GatewayVerticle that has all the logic corresponding to how our gateway will handle the request and MicroserviceVerticle (Photogram, and UserService) that has their own logic.
Let’s first dive deep into the GatewayVerticle. You can see that our verticle extends AbstractVerticle. This is done so that we don’t have to write our own Verticle from scratch. All we need to do is override the start() method and write our own logic in it. When the Verticle is deployed, its start method is called. You can see that in the start method, we are initializing a Router.
A Router is used to handle the HTTP Request; it is part of the vert.x-web module, which we added in our dependency. One can route the request based on paths, for example, /api/users, /services/:id.
We can also route based on HTTP methods like GET, POST, PUT, etc.
In our case, we have defined a simple path-based route with a path parameter and regex, and called it DEFAULT_MOUNT_PATH. Any request that has /services/:service/anything/example/etc will be served by this very router.
Let’s take an example: since our gateway is running on localhost:8080, if we hit localhost:8080/services/photogram/post, our router will intercept it. If we hit localhost:8080/something/photogram/post, our router will fail to intercept it and will throw Resource not found.
Post that, we have defined handlers; in our case, we have defined three handlers:
All these handlers are called one by one in serial order. You can create just one handler and put all the logic in it, but here, I have created three of them to segregate the logic.
The validator handler validates if the request that is coming is for a valid service. Since we have only onboarded two services: photogram and user-service, if we get a request for, say, post-service, we will fail the request and won’t let it proceed to the next handlers.
The forwarder handler basically creates an HTTP client and sends the request to the respective upstream. If a request came for the photogram service on the gateway, our gateway will find the host and port of that service, append the path to it, create a requestUri, and send the request.
The endRouter handler is just a way to know that we have completed the request. We can skip it as well.
Defining a router is okay, but we need to create an HTTP server that will actually listen for those requests, and that is where we will create our gateway server and bind it to the router.
Vert.x provides a method called createHttpServer(); we then bind our router to it using requestHandler() that takes a router object and listens to port 80 or whatever we want.
I will leave the code of validator, forwarder, and the endRouter handlers for the reader to understand. The code is heavily commented for ease of readability.
GatewayVerticle.java1package org.example.verticle;
2
3import com.google.common.base.Strings;
4import io.vertx.core.AbstractVerticle;
5import io.vertx.core.AsyncResult;
6import io.vertx.core.Promise;
7import io.vertx.core.buffer.Buffer;
8import io.vertx.core.http.HttpMethod;
9import io.vertx.core.http.HttpServer;
10import io.vertx.core.http.HttpServerOptions;
11import io.vertx.core.json.JsonObject;
12import io.vertx.ext.web.Router;
13import io.vertx.ext.web.RoutingContext;
14import io.vertx.ext.web.client.HttpRequest;
15import io.vertx.ext.web.client.HttpResponse;
16import io.vertx.ext.web.client.WebClient;
17import lombok.extern.slf4j.Slf4j;
18import org.example.model.config.ServerConfig;
19import org.example.model.endpoint.EndpointType;
20import org.example.model.config.MicroserviceConfig;
21import org.example.model.endpoint.StaticEndpoint;
22
23import java.util.Objects;
24import java.util.UUID;
25
26@Slf4j
27public class GatewayVerticle extends AbstractVerticle {
28 private final ServerConfig serverConfig;
29 public static String DEFAULT_MOUNT_PATH = "/services/:service/*";
30
31 private WebClient webClient;
32
33 public GatewayVerticle(ServerConfig serverConfig) {
34 this.serverConfig = serverConfig;
35 }
36
37 @Override
38 public void start(Promise<Void> startPromise) throws Exception {
39 // create a router
40 Router router = Router.router(vertx);
41 this.webClient = WebClient.create(vertx);
42 // mount this router for the requests that begins with /services (because that is convention we are going to follow)
43 // if somebody comes at some other path say /random/* we will fail it
44
45 // when someone will come to /services/name-of-service/anything,
46 // our router will come into the picture, and it will go through all these handlers one by one
47 // handlers are meant for a lot of thing, we have a validator handler that will check whether the request is correct
48 // forward handler that forwards the request to the upstream, and
49 // end handler simply just marks the end of the request
50 router.route(DEFAULT_MOUNT_PATH)
51 .handler(this::validator)
52 .handler(this::forwarder)
53 .handler(this::endRouter);
54
55 // the above was just a router, but we want to create an HTTP server
56 // that will listen and when the request will land, our router will get triggered
57 HttpServerOptions defaultHttpServerOptions = new HttpServerOptions();
58 HttpServer server = vertx.createHttpServer(defaultHttpServerOptions);
59
60 server.requestHandler(router)
61 .listen(serverConfig.getVertxConfig().getDefaultPort());
62 }
63
64
65 public void validator(RoutingContext routingContext) {
66 // let's understand few things here
67 // /services/:service/:path
68 String service = routingContext.pathParam("service");
69 String path = routingContext.pathParam("*");
70
71 String requestId = routingContext.get("requestId");
72 if (Strings.isNullOrEmpty(requestId)) {
73 requestId = UUID.randomUUID().toString();
74 routingContext.put("requestId", requestId);
75 }
76
77 // let's log the request for which service this request was targeted for and for what endpoint
78 log.info("Received request on service: {}, for path: {}, with requestId: {}", service, path, requestId);
79 // check if the service is even onboarded or visible to gateway?
80 if (serverConfig.getServiceConfigs().containsKey(service)) {
81 // we are just printing the info here
82 // you can forward the request to the upstream by fetching the endpoint type and all
83 // putting this information so that the upcoming handler have this information
84 routingContext.put("service", service);
85 routingContext.put("path", path);
86 // move to the next handler
87 routingContext.next();
88 } else {
89 // fail the request by saying the service is not present
90 routingContext.response()
91 .end(new JsonObject()
92 .put("status", 404)
93 .put("error", String.format("Service '%s' not found", service))
94 .encode());
95 }
96
97 }
98
99 public void forwarder(RoutingContext routingContext) {
100 // this handler forwards the request to the upstream
101 String requestUri = buildRequestUri(routingContext);
102
103 log.info("Upstream Request URI: {}", requestUri);
104 HttpRequest<Buffer> httpRequest = getHttpRequest(routingContext, requestUri);
105 httpRequest.send(clientResponse -> handleResponse(routingContext, clientResponse));
106 }
107
108 private void handleResponse(RoutingContext routingContext, AsyncResult<HttpResponse<Buffer>> clientResponse) {
109 // this method will be called when we receive a response from the client
110 if (clientResponse.succeeded()) {
111 HttpResponse<Buffer> httpResponse = clientResponse.result();
112 routingContext.response().setStatusCode(200);
113 routingContext.response().end(httpResponse.bodyAsString());
114 } else {
115 routingContext.response().setStatusCode(500);
116 log.error("Error while handling response", clientResponse.cause());
117 }
118 routingContext.next();
119 }
120
121 public void endRouter(RoutingContext routingContext) {
122 log.info("Ending Request");
123 }
124
125
126 public MicroserviceConfig getServiceConfig(RoutingContext routingContext) {
127 String service = routingContext.pathParam("service");
128 if (Strings.isNullOrEmpty(service)) {
129 throw new IllegalArgumentException("Missing required parameter 'service'");
130 }
131 return serverConfig.getServiceConfigs().get(service);
132 }
133
134 public String buildRequestUri(RoutingContext routingContext) {
135
136 MicroserviceConfig microserviceConfig = getServiceConfig(routingContext);
137 // find the host and port for the service where the request needs to be forwarded
138 // we are only covering the static endpoint part
139 // in order to build up the complete request, we need to first get the host and port of the microservice and
140 // then append the path to it
141 // host:port/path
142 if (Objects.requireNonNull(microserviceConfig.getEndpoint().getType()) == EndpointType.STATIC) {
143 StaticEndpoint endpoint = (StaticEndpoint) microserviceConfig.getEndpoint();
144 // http scheme + host + port + path
145 return String.format("%s://%s:%s/%s", "http", endpoint.getHost(), endpoint.getPort(), routingContext.get("path"));
146 }
147 return "";
148 }
149
150 public HttpRequest<Buffer> getHttpRequest(RoutingContext routingContext, String requestUri) {
151 HttpMethod method = routingContext.request().method();
152 switch (String.valueOf(method)) {
153 case "GET" -> {
154 return webClient.getAbs(requestUri);
155 }
156 case "POST" -> {
157 return webClient.postAbs(requestUri);
158 }
159 default -> throw new IllegalStateException("Unexpected value: " + method);
160 }
161 }
162
163}
Now let’s move on to the microservices. We have created two of them: Photogram and UserService. Both of these resemble the GatewayVerticle; the difference is that they are simply creating routes, logging the requests, and returning responses.
In Photogram, we have created two routes: /post and /view. Once done, we then spin up an HTTP server and register these routes to it.
Photogram.java1package org.example.microservice;
2
3import io.vertx.core.AbstractVerticle;
4import io.vertx.core.Promise;
5import io.vertx.core.json.JsonArray;
6import io.vertx.core.json.JsonObject;
7import io.vertx.ext.web.Router;
8import lombok.extern.slf4j.Slf4j;
9import org.example.model.config.ServerConfig;
10import org.example.model.config.MicroserviceConfig;
11import org.example.model.endpoint.StaticEndpoint;
12
13import java.util.List;
14
15@Slf4j
16public class Photogram extends AbstractVerticle {
17 // this verticle will act like a microservice (postgram)
18 private final ServerConfig serverConfig;
19
20 public Photogram(ServerConfig serverConfig) {
21 this.serverConfig = serverConfig;
22 }
23
24 @Override
25 public void start(Promise<Void> startPromise) throws Exception {
26 super.start(startPromise);
27 MicroserviceConfig postgramConfig = serverConfig.getServiceConfigs().get("photogram");
28 Router router = Router.router(vertx);
29
30 // our photogram service will expose two apis
31 // 1. one to post the post and other to view the post
32 // 2. please note we are not getting into the nitty-gritty of the whole service we are just mocking them
33 router.route("/post").handler(context -> {
34 log.info("Creating a new Post on Photogram");
35 context.json(new JsonObject().put("message", "Successfully Created a Post"));
36 });
37
38 router.route("/view").handler(context -> {
39 log.info("Received a view request on Photogram");
40 context.json(new JsonObject().put("posts", new JsonArray(List.of("P21, P45, P68"))));
41 });
42
43 vertx.createHttpServer()
44 .requestHandler(router)
45 .listen(((StaticEndpoint) postgramConfig.getEndpoint()).getPort());
46
47 }
48}
49
1package org.example.microservice;
2
3import io.vertx.core.AbstractVerticle;
4import io.vertx.core.Promise;
5import io.vertx.core.json.JsonObject;
6import io.vertx.ext.web.Router;
7import org.example.model.config.ServerConfig;
8import org.example.model.config.MicroserviceConfig;
9import org.example.model.endpoint.StaticEndpoint;
10
11public class UserService extends AbstractVerticle {
12 private final ServerConfig serverConfig;
13
14 public UserService(ServerConfig serverConfig) {
15 this.serverConfig = serverConfig;
16 }
17
18 @Override
19 public void start(Promise<Void> startPromise) throws Exception {
20 super.start(startPromise);
21
22 MicroserviceConfig userServiceConfig = serverConfig.getServiceConfigs().get("user-service");
23
24 Router router = Router.router(vertx);
25 // we'll be exposing yet another service called user service that will manage our users such as creating, signing up etc
26 router.route("/create").handler(routingContext -> {
27 routingContext.json(new JsonObject().put("message", "Successfully Registered an User"));
28 });
29
30 vertx.createHttpServer()
31 .requestHandler(router)
32 .listen(((StaticEndpoint) userServiceConfig.getEndpoint()).getPort());
33
34 }
35}
36
Let’s now spin up our gateway. We’ll see the following logs:
Startup logs
You can see in the logs that we are getting Photogram Service deployed and User Service deployed.
Now, let’s go to the web browser and hit localhost:8080 and see what we get. We got Resource Not Found, but why?
Because we haven’t registered any router for "/" path. Let’s go to the Photogram service and hit its /post API and see what it returns.
In order to hit that API, we need to visit localhost:8080/services/photogram/post , and we get the following message on our browser.
Response from Postgram Service
What if we try to hit a service that isn’t registered or onboarded onto the gateway? Let’s try to hit post-service with the /post API.
The endpoint we need is localhost:8080/services/post-service/post .
You can see that we got a 404 - Service not found message from the gateway.
Error from gateway
Now, since this was a very basic example of an API gateway, it lacks a few things that I will let the user explore, such as:
You can visit my GitHub repo to view the complete source code: https://github.com/confusedconsciousness/gateway .
I hope this article helped you gain insight into what a gateway does and a little bit about the Vert.x toolkit as well.
References:
Subscribe to the newsletter to learn more about the decentralized web, AI and technology.
Please be respectful!