API Gateway in PHP
What does an API gateway do?
An API Gateway (AG) is a server that acts as a single point of entry for a set of microservices.
API Gateway receives client requests, forwards them to the appropriate microservice, and then returns the server’s response to the client.
API Gateway is responsible for tasks such as routing, authentication, and rate limiting. This enables microservices to focus on their individual tasks and improves the overall performance and scalability of the system.
Then we can say in a word “API gateway is an API management tool that sits between a client and a collection of backend services”
Why use an API gateway?
Most enterprise APIs are deployed via API gateways. It’s common for API gateways to handle common tasks that are used across a system of API services, such as user authentication, rate limiting, and statistics.
At its most basic, an API service accepts a remote request and returns a response. But real life is never that simple. Consider your various concerns when you host large-scale APIs.
- You want to protect your APIs from overuse and abuse, so you use an authentication service and rate limiting.
- You want to understand how people use your APIs, so you’ve added analytics and monitoring tools.
- If you have monetized APIs, you’ll want to connect to a billing system.
- You may have adopted a microservices architecture, in which case a single request could require calls to dozens of distinct applications.
- Over time you’ll add some new API services and retire others, but your clients will still want to find all your services in the same place.
Your challenge is offering your clients a simple and dependable experience in the face of all this complexity. An API gateway is a way to decouple the client interface from your backend implementation. When a client makes a request, the API gateway breaks it into multiple requests, routes them to the right places, produces a response, and keeps track of everything.
Here is the top API Gateway uses:
𝗥𝗼𝘂𝘁𝗶𝗻𝗴: The API Gateway receives requests from clients and routes them to the appropriate microservice. This enables clients to access the various microservices through a single entry point, simplifying the overall system design.
𝗦𝗲𝗰𝘂𝗿𝗶𝘁𝘆: The API Gateway can be used to authenticate clients and enforce access control policies for the microservices. This helps to ensure that only authorized clients can access the microservices and helps to prevent unauthorized access.
𝐓𝐫𝐚𝐧𝐬𝐟𝐨𝐫𝐦𝗶𝗻𝗴 𝐫𝐞𝐪𝐮𝐞𝐬𝐭𝐬 𝐚𝐧𝐝 𝐫𝐞𝐬𝐩𝐨𝐧𝐬𝐞𝐬: API Gateway can transform incoming requests and outgoing responses to and from the backend to meet the needs of different clients or to comply with different backend architectures.
𝗥𝗮𝘁𝗲 𝗹𝗶𝗺𝗶𝘁𝗶𝗻𝗴: You can rate limit client access to microservices with an API Gateway. This can help prevent denial of service attacks and other types of malicious behavior.
𝗟𝗼𝗮𝗱 𝗯𝗮𝗹𝗮𝗻𝗰𝗶𝗻𝗴: The API Gateway can distribute incoming requests among multiple instances of a microservice, enabling the system to handle a larger number of requests and improving its overall performance and scalability.
𝗖𝗮𝗰𝗵𝗶𝗻𝗴: The API Gateway can cache responses from the microservices, reducing the number of requests that need to be forwarded to the microservices and improving the overall performance of the system.
𝗦𝗲𝗿𝘃𝗲𝗿𝗹𝗲𝘀𝘀 𝗲𝘅𝗲𝗰𝘂𝘁𝗶𝗼𝗻: API Gateway can integrate with other services, such as AWS Lambda, to enable serverless architectures and enable complex processing of requests without requiring a dedicated server.
𝗖𝗶𝗿𝗰𝘂𝗶𝘁 𝗯𝗿𝗲𝗮𝗸𝗲𝗿: API Gateway can be used to implement circuit breaker patterns, which can help to protect against cascading failures and improve the resilience of your system.
𝗥𝗲𝘃𝗲𝗿𝘀𝗲 𝗽𝗿𝗼𝘅𝘆: API Gateway can act as a reverse proxy, routing incoming requests to the appropriate backend service based on the request path or other criteria.
𝗔𝗣𝗜 𝘃𝗲𝗿𝘀𝗶𝗼𝗻𝗶𝗻𝗴: API Gateway can be used to implement API versioning, allowing you to maintain multiple versions of an API, and manage the transition from one version to another.
Implementation
I'll try to explain with a basic example for API
.
Let’s say you have currently 3 microservices
:
- Users
- Posts
- Core
I assume you’re using http Only
cookie to store user token.
In Core microservice
I have this route structure:
Route::prefix('core')->group(function () {
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::middleware('scope.trader')->group(function () {
Route::get('user', [AuthController::class, 'user']);
});
});
Now I want to login which I should send an API
request and I should think of a solution to send token
anytime I need it.
- login(this is where you get token) and register don’t need token
- user needs a token (this is where you asked for a solution)
So in addition to getting a result, I should create a service for a user, and here how I’ve done it :
UserService :
class UserService extends ApiService
{
public function __construct()
{
// Get User Endpoint Microservice API URL
$this->endpoint = env('USERS_MS') . '/api';
}
}
ApiService :
abstract class ApiService
{
protected string $endpoint;
public function request($method, $path, $data = [])
{
$response = $this->getRequest($method, $path, $data);
if ($response->ok()) {return $response->json();};
throw new HttpException($response->status(), $response->body());
}
public function getRequest($method, $path, $data = [])
{
return \Http::acceptJson()->withHeaders([
'Authorization' => 'Bearer ' . request()->cookie('token')
])->$method("{$this->endpoint}/{$path}", $data);
}
public function post($path, $data)
{
return $this->request('post', $path, $data);
}
public function get($path)
{
return $this->request('get', $path);
}
public function put($path, $data)
{
return $this->request('put', $path, $data);
}
public function delete($path)
{
return $this->request('delete', $path);
}
}
I’ll try to explain with a basic example for API
.
Let’s say you have currently 3 microservices
:
- Users
- Posts
- Core
I assume you’re using httpOnly
cookie to store user token.
In Core microservice
I have this route structure:
Route::prefix('core')->group(function () {
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::middleware('scope.trader')->group(function () {
Route::get('user', [AuthController::class, 'user']); });
});
Now i want to login which I should send an API
request and I should think of a solution to send token
anytime I need it.
- login(this is where you get token) and register don’t need token
- user needs token (this is where you asked for a solution)
So in addition to get a result, I should create a service for user, and here how I’ve done it :
UserService :
class UserService extends ApiService
{
public function __construct()
{
// Get User Endpoint Microservice API URL
$this->endpoint = env('USERS_MS') . '/api';
}
}
ApiService :
abstract class ApiService
{
protected string $endpoint;
public function request($method, $path, $data = [])
{
$response = $this->getRequest($method, $path, $data); if ($response->ok()) {return $response->json();}; throw new HttpException($response->status(), $response->body());
} public function getRequest($method, $path, $data = [])
{
return \Http::acceptJson()->withHeaders([
'Authorization' => 'Bearer ' . request()->cookie('token')
])->$method("{$this->endpoint}/{$path}", $data);
} public function post($path, $data)
{
return $this->request('post', $path, $data);
} public function get($path)
{
return $this->request('get', $path);
} public function put($path, $data)
{
return $this->request('put', $path, $data);
} public function delete($path)
{
return $this->request('delete', $path);
}
}
If you’re wondering where this UserService
come from, then I should say, I've created a package to use it in other microservices, so you can do the same or just create a service and use it in your microservices or etc.
Everything is obvious about ApiService
, but I'll try to explain the base.
- Anytime we want to do an
API
call, we can simply callAllowed methods
in this class, then our methods, will call requests, to pass common arguments, and eventually use those arguments to do theAPI
call. getRequest
the method is doing the call and getting the stored token fromhttpOnly
cookie, and will send it as anAuthorization
header to the target endpoint, and eventually it'll return whatever it get from the target.
So If we want to use this, we can simply do like this in our controller :
class AuthController extends Controller
{
// use Services\UserService;
public UserService $userService;
/**
* @param UserService $userService
*/
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public function register(RegisterRequest $request)
{
$data = $request->only('name', 'email', 'password') + ['additional_fileds' => 0 ];
// additional fields can be used for something except from request and
// optional, like is it admin or user or etc.
// call the post method, pass the endpoint url(`register`), pass $data
$user = $this->userService->post('register', $data);
// get data from target endpoint
// and ...
return response($user, Response::HTTP_CREATED);
}
public function login(Request $request)
{
// same thing here again, but this time i passed scope to help me
// get the specific user scope
$data = $request->only('email', 'password') + ['scope' => 'writer'];
$response = $this->userService->post('login', $data);
// as you can see when user do success login, we will get token,
// which i got that token using Passport and set it to $cookie
$cookie = cookie('token', $response['token'], 60 * 24); // 1 day
// then will set a new httpOnly token on response.
return response([
'message' => 'success'
])->withCookie($cookie);
}
public function user(Request $request)
{
// Here, base on userService as you saw, we passed token in all requests
// which if token exist, we get the result, since we're expecting
// token to send back the user informations.
$user = $this->userService->get('user');
// get posts belong to authenticated user
$posts = Post::where('user_id', $user['id'])->get();
$user['posts'] = $posts;
return $user;
}
}
I’ll try to explain with a basic example for API
.
Let’s say you have currently 3 microservices
:
- Users
- Posts
- Core
I assume you’re using httpOnly
cookie to store user token.
In Core microservice
I have this route structure:
Route::prefix('core')->group(function () {
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::middleware('scope.trader')->group(function () {
Route::get('user', [AuthController::class, 'user']); });
});
Now I want to login which I should send an API
request and I should think of a solution to send token
anytime I need it.
- login(this is where you get token) and register don’t need token
- user need token (this is where you asked for a solution)
So in addition to get a result, I should create a service for the user, and here how I’ve done it :
UserService :
class UserService extends ApiService
{
public function __construct()
{
// Get User Endpoint Microservice API URL
$this->endpoint = env('USERS_MS') . '/api';
}
}
ApiService :
abstract class ApiService
{
protected string $endpoint;
public function request($method, $path, $data = [])
{
$response = $this->getRequest($method, $path, $data); if ($response->ok()) {return $response->json();}; throw new HttpException($response->status(), $response->body());
} public function getRequest($method, $path, $data = [])
{
return \Http::acceptJson()->withHeaders([
'Authorization' => 'Bearer ' . request()->cookie('token')
])->$method("{$this->endpoint}/{$path}", $data);
} public function post($path, $data)
{
return $this->request('post', $path, $data);
} public function get($path)
{
return $this->request('get', $path);
} public function put($path, $data)
{
return $this->request('put', $path, $data);
} public function delete($path)
{
return $this->request('delete', $path);
}
}
If you’re wondering where, this UserService
come from, then I should say, I've created a package to use it in other microservices, so you can do the same or just create a service and use it in your microservices or etc.
Everything is obvious about ApiService
, but I'll try to explain the base.
- Anytime we want to do an
API
call, we can simply callAllowed methods
in this class, then our methods, will call request, to pass common arguments, and eventually using those arguments to do theAPI
call. getRequest
the method is doing the call and get the stored token fromhttpOnly
cookie, and will send it as anAuthorization
header to the target endpoint, and eventually it'll return whatever it get from target.
So If we want to use this, we can simply do like this in our controller :
class AuthController extends Controller
{
// use Services\UserService;
public UserService $userService;
/**
* @param UserService $userService
*/
public function __construct(UserService $userService)
{
$this->userService = $userService;
} public function register(RegisterRequest $request)
{
$data = $request->only('name', 'email', 'password') + ['additional_fileds' => 0 ];
// additional fields can be used for something except from request and
// optional, like is it admin or user or etc. // call the post method, pass the endpoint url(`register`), pass $data
$user = $this->userService->post('register', $data);
// get data from target endpoint
// and ...
return response($user, Response::HTTP_CREATED);
} public function login(Request $request)
{
// same thing here again, but this time i passed scope to help me
// get the specific user scope
$data = $request->only('email', 'password') + ['scope' => 'writer']; $response = $this->userService->post('login', $data);
// as you can see when user do success login, we will get token,
// which i got that token using Passport and set it to $cookie
$cookie = cookie('token', $response['token'], 60 * 24); // 1 day
// then will set a new httpOnly token on response.
return response([
'message' => 'success'
])->withCookie($cookie);
} public function user(Request $request)
{
// Here, base on userService as you saw, we passed token in all requests
// which if token exist, we get the result, since we're expecting
// token to send back the user informations. $user = $this->userService->get('user'); // get posts belong to authenticated user
$posts = Post::where('user_id', $user['id'])->get(); $user['posts'] = $posts; return $user;
}
}
Now, how about user microservice? well, Everything is clear here, and it should work like a basic app.
Here’s the routes :
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::middleware(['bunch','of', 'middlewares'])->group( function (){
Route::get('user', [AuthController::class, 'user']);
});
And in controller :
class AuthController extends Controller
{
public function register(Request $request)
{
$user = User::create(
$request->only('first_name', 'email', 'additional_field')
+ ['password' => \Hash::make($request->input('password'))]
);
return response($user, Response::HTTP_CREATED);
}
public function login(Request $request)
{
if (!\Auth::attempt($request->only('email', 'password'))) {
return response([
'error' => 'user or pass is wrong or whatever.'
], Response::HTTP_UNAUTHORIZED);
}
$user = \Auth::user();
$jwt = $user->createToken('token', [$request->input('here you can pass the required scope like trader as i expalined in top')])->plainTextToken;
return compact('token');
}
public function user(Request $request)
{
return $request->user();
}
}
So here’s the complete example and you can use the Core microservice
approach on other microservices to get your information related to the authenticated user, and as you can see everything will be authenticated due to those requests
from core
to other microservices.