Often times APIs are secured with a unique APIKEY for each application. The requirement of these APIs is you send your APIKEY with every request either through the query string or in a custom header. But when your application is a single page application (SPA) you cannot embed your APIKEY into your application because it is not a secure way to handle the sensitive data and CORS support is not enabled.
To solve this problem of handling secure data while writing a SPA consumer you should use a proxy application. The proxy will take all requests from the SPA, secure them with CORS, append the APIKEY, and forward the request to the secured API. And it will forward the response from the API back to the requesting SPA.
We will be making an API call to the Yahoo! Finance API at RapidAPI. This API uses an APIKEY and is a good example for this tutorial. The technique used here may be applied to any API which requires an APIKEY.
As part of the HTTP Specification APIs may make requests including with the following HTTP methods:
- GET
- POST
- PATCH
- PUT
- DELETE
A proxy application may handle any or all of these five HTTP verbs. This article describes how to build a GET proxy applicaiton for API use.
The proxy controller takes a single urlencoded parameter, url
, at a single RPC endpoint in this format:
https://proxy.server/proxy?url=%2Fmarket%2Fget-summary%2F1
Your ProxyAPI RPC may then handle the url and append the APIKEY to the request then forward it on to the data API. This allows you to write queries as though they were directly on the API.
Example Proxy Code
This controller action, written in PHP in the Zend Framework, implements a proxy to the API for GET requests. Please see the comments throughout the code.
use Exception; use ZendMvcControllerAbstractActionController; use ZendUriUriFactory; use ZendHttpClient; use ZendHttpRequest; use ZendHttpResponse; use ZFApiProblemApiProblem; use ZFApiProblemApiProblemResponse; class ProxyController extends AbstractActionController { private $config = [ 'x-rapidapi-host' => 'apidojo-yahoo-finance-v1.p.rapidapi.com', 'x-rapidapi-key' => "12345678901234567890123456789012345678901234567890", 'region' => 'US', 'language' => 'en', ]; public function proxyAction() { $url = $this->params()->fromQuery('url'); // Extract the URI, append region and language to request $uri = UriFactory::factory('https://' . $this->config['x-rapidapi-host'] . '/' . $url); $query = $uri->getQueryAsArray(); $query['region'] = $this->config['region']; $query['lang'] = $this->config['language']; $uri->setQuery($query); // Run proxy based on the request method which with this was called. switch ($this->getRequest()->getMethod()) { case 'GET': // Here would be a good spot to add caching // Handle GET query requests with no body $client = new Client((string) $uri); $client->setMethod($this->getRequest()->getMethod()); // Copy all headers from request to this server into the // request for the API call. Append x-rapidapi* headers $client->getRequest() ->getHeaders() ->addHeaders($this->getRequest()->getHeaders()) ->addHeaderLine('Accept', 'application/json') ->addHeaderLine('x-rapidapi-key', $this->config['x-rapidapi-key']) ->addHeaderLine('x-rapidapi-host', $this->config['x-rapidapi-host']) ; try { // Try making the API call $response = $client->send(); if ($response->getStatusCode() !== 200) { // Use API Problem to return a non-200 status code which did not // trigger an exception. $apiProblem = new ApiProblem( $response->getStatusCode(), $response->getBody() ); $apiProblemResponse = new ApiProblemResponse($apiProblem); return $apiProblemResponse; } } catch (Exception $e) { // Handle all exceptions with ApiProblem $apiProblem = new ApiProblem(500, $e->getMessage() . ' ' . (string) $uri); $apiProblemResponse = new ApiProblemResponse($apiProblem); return $apiProblemResponse; } // Return the response from the api directly to the requsting // API consumer return $response; case 'POST': case 'PATCH': case 'PUT': case 'DELETE': default: return $this->getResponse(); } } }
CORS
| CORS is a technique that permits resource sharing between scripts running on a browser client and resources from a different origin.
CORS is used in browser-to-api communications. CORS is not used in server-to-api communication. Because SPAs run entirely in the browser they rely on proper CORS headers to access the proxy API. There are many libraries to implement CORS. The module I use is https://github.com/zf-fr/zfr-cors. You will need to implement CORS or you will see an error similar to No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
Making calls to the proxy from the SPA
Here is a TypeScript snippet which uses a proxy for an API call to the Yahoo! API on RapidAPI:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { environment } from '@env'; /** * For brevity the MarketSymbol class does not include all fields from * an API response. */ class MarketSymbol { symbol: string; fullExchangeName: string; market: string; regularMarketPrice: { raw: number; fmt: number; }; } /** * This is the structure of the response expected from the * MarketSummary API call. */ class MarketSummary { marketSummaryResponse: { result: Array<MarketSymbol>; error: any; }; } @Injectable({ providedIn: 'root' }) export class YahooFinanceProxyService { /** * The configuration looks like */ // export const environment = { // production: false, // apiUrl: 'https://proxy.server/proxy?url=' // } private apiUrl = environment.apiUrl; constructor( private http: HttpClient ) { } public getMarketSummary(): Observable<MarketSummary> { { return this.http.get<MarketSummary>(this.apiUrl + encodeUriComponent('/market/get-summary')); } }
Other approaches to proxy
Apache HTTP and nginx include a proxy mod. It should be possible to create a configuration so all incoming traffic has the APIKEY appended to the request which is then forwarded to the API server. CORS would also need to be handled through the proxy configuration. This is outside the scope of this article.
Conclusion
As SPAs have now matured it will be common to access APIs with them. But until more developers implement OAuth2, instead of an APIKEY, proxies to APIs are a time-saving device to avoid coding the API as a server application which is then served as an API to the SPA.
How is the proxy protected from being used by other apps than my SPA? Is it CORS that enables ONLY my SPA to use that proxy?