DEV Community

Cover image for The easy way to create API Gateway
Serhii Korol
Serhii Korol

Posted on

The easy way to create API Gateway

Hi! Nowadays, Microservices are widely used in software development. When you need to use several clients for API, the best solution is to use API Gateway. I'll show you how fast and straightforward it is to implement API Gateway.

What is this API Gateway?

The API Gateway is a service that communicates between clients and business logic. In other words, clients don't have direct access to business logic and their API. The API Gateway handles all input requests and redirects them to the appropriate microservice. Why is this complication needed? The API Gateway gives additional security and can handle and validate input requests, modify requests and responses, route requests, handle exceptions, and log them in one place.

API Gateway schema

In real life, the API Gateway is complicated enough to implement. However, there is an existing, straightforward approach to implementing it. You only need to adjust it according to your needs and requirements.

Implementing

You must create a separate project and install this AspNetCore.ApiGateway NuGet package for implementation. The base implementation consists of only one method, which should receive the IApiOrchestrator argument. This argument uses three main methods: AddApi, AddHub, and AddEventSource. I'll be using a more straightforward API.

public static class OrchestrationService
{
    public static void Create(IApiOrchestrator orchestrator, IApplicationBuilder app)
        {
            orchestrator.AddApi("weatherservice", "https://localhost:5003/")
                                .AddRoute("forecast", GatewayVerb.GET, new RouteInfo { Path = "weatherforecast/forecast", ResponseType = typeof(IEnumerable<WeatherForecast>) })
                                .AddRoute("add", GatewayVerb.POST, new RouteInfo { Path = "weatherforecast/summaries/add", RequestType = typeof(WeatherSummaryRequest), ResponseType = typeof(string[]) });
        }
}
Enter fullscreen mode Exit fullscreen mode

The next step is to configure and register this service.

builder.Services.AddApiGateway(options =>
{
    options.UseResponseCaching = false;
    options.ResponseCacheSettings = new ApiGatewayResponseCacheSettings
    {
        Duration = 60,
        Location = ResponseCacheLocation.Any,
        VaryByQueryKeys = ["apiKey", "routeKey"]
    };
});

app.UseApiGateway(orchestrator => OrchestrationService.Create(orchestrator, app));
Enter fullscreen mode Exit fullscreen mode

It's mega simple of base functionality. Sure, you need authorization, handlers, and validators.

Authorization

There are two ways to authorize your API: HTTP verb or global.

builder.Services.AddScoped<IGatewayAuthorization, AuthorizationService>();
builder.Services.AddScoped<IGetOrHeadGatewayAuthorization, GetAuthorizationService>();

public class AuthorizationService : IGatewayAuthorization
    {
        public async Task AuthorizeAsync(AuthorizationFilterContext context, string apiKey, string routeKey, string verb)
        {
            await Task.CompletedTask;
        }
    }

    public class GetAuthorizationService : IGetOrHeadGatewayAuthorization
    {
        public async Task AuthorizeAsync(AuthorizationFilterContext context, string apiKey, string routeKey)
        {
            await Task.CompletedTask;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Action filters

The same goes for action filters, but each HTTP verb has a separate interface.

builder.Services.AddScoped<IGatewayActionFilter, ActionFilterService>();
builder.Services.AddScoped<IPostGatewayActionFilter, PostActionFilterService>();

public class ActionFilterService : IGatewayActionFilter
    {
        public async Task OnActionExecutionAsync(ActionExecutingContext context, string apiKey, string routeKey, string verb)
        {
            await Task.CompletedTask;
        }
    }

    public class PostActionFilterService : IPostGatewayActionFilter
    {
        public async Task OnActionExecutionAsync(ActionExecutingContext context, string apiKey, string routeKey)
        {
            await Task.CompletedTask;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Exception filters

A similar approach can also register the exception filters.

builder.Services.AddScoped<IGatewayExceptionFilter, ExceptionFilterService>();
builder.Services.AddScoped<IPostGatewayExceptionFilter, PostExceptionFilterService>();

public class ExceptionFilterService : IGatewayExceptionFilter
    {
        public async Task OnExceptionAsync(ExceptionContext context, string apiKey, string routeKey, string verb)
        {
            await Task.CompletedTask;
        }
    }

    public class PostExceptionFilterService : IPostGatewayExceptionFilter
    {
        public async Task OnExceptionAsync(ExceptionContext context, string apiKey, string routeKey)
        {
            await Task.CompletedTask;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Result filters

For handling results, use this.

builder.Services.AddScoped<IGatewayResultFilter, ResultFilterService>();
builder.Services.AddScoped<IPostGatewayResultFilter, PostResultFilterService>();

public class ResultFilterService : IGatewayResultFilter
    {
        public async Task OnResultExecutionAsync(ResultExecutingContext context, string apiKey, string routeKey, string verb)
        {
            await Task.CompletedTask;
        }
    }

    public class PostResultFilterService : IPostGatewayResultFilter
    {
        public async Task OnResultExecutionAsync(ResultExecutingContext context, string apiKey, string routeKey)
        {
            await Task.CompletedTask;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Hub filters

If you use Signalr, you'll can use this:

builder.Services.AddScoped<IGatewayHubFilter, GatewayHubFilterService>();

public class GatewayHubFilterService : IGatewayHubFilter
    {
        public ValueTask<object> InvokeMethodAsync(HubInvocationContext invocationContext)
        {
            return new ValueTask<object>();
        }

        public Task OnConnectedAsync(HubLifetimeContext context)
        {
            return Task.CompletedTask;
        }

        public Task OnDisconnectedAsync(HubLifetimeContext context, Exception exception)
        {
            return Task.CompletedTask;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Middleware

For middleware, use this:

builder.Services.AddTransient<IGatewayMiddleware, GatewayMiddlewareService>();

public class GatewayMiddlewareService : IGatewayMiddleware
    {
        public async Task Invoke(HttpContext context, string apiKey, string routeKey)
        {
            await Task.CompletedTask;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Swagger

Under the hood of the NuGet package is an implemented controller for most HTTP verbs, and the Swagger gets all these operators. However, you do not need all HTTP verbs: GET, POST, PUT, PATCH, HEAD, DELETE. What if you need only GET and POST? To resolve this task, you can create a custom Swagger filter. The API Gateway also has the query parameter you don't need. This filter can filter specified operators and parameters.

builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "My Api Gateway", Version = "v1" });
    c.DocumentFilter<SwaggerDocumentFilter>();
});

public class SwaggerDocumentFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        var verbs = new[] { OperationType.Get, OperationType.Post };
        foreach (var path in swaggerDoc.Paths)
        {
            if (path.Key.StartsWith("/api/Gateway/{apiKey}") || path.Key.StartsWith("/api/Gateway/orchestration"))
            {
                var operationsToRemove = path.Value.Operations
                    .Where(o => !verbs.Contains(o.Key))
                    .Select(o => o.Key)
                    .ToList();

                foreach (var operation in operationsToRemove)
                {
                    path.Value.Operations.Remove(operation);
                }

                foreach (var operation in path.Value.Operations)
                {
                    var queryParam = operation.Value.Parameters.FirstOrDefault(p => p.In == ParameterLocation.Query);
                    if (queryParam != null)
                    {
                        operation.Value.Parameters.Remove(queryParam);
                    }
                }
            }
            else
            {
                swaggerDoc.Paths.Remove(path.Key);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Without this filter, you'll see all endpoints, but your microservice doesn't use Signalr and has only GET and POST methods.

swagger without filter

You also don't need a query parameter that was implemented by default.

get

With this filter, you can show only those methods that you need.

Filtered swagger

And you can hide unused query parameters:

hide

Pros and cons

This package definitely can be useful in software development when you don't need high flexibility.

Pros

  • simple
  • free

Cons

  • low flexibility: I want to set only needed operators
  • not working with type string request
  • default parameters for all operators
  • you should adjust your swagger

Source code: link
About the project: link

Happy coding!

Buy Me A Beer

Top comments (0)