Why you should (almost) never use an absolute path to your APIs again

As a web developer, you‘ve probably typed out your fair share of absolute paths when making API requests from your front-end code. Something like:

const API_URL = ‘https://api.myapp.com/users‘;

fetch(API_URL)
  .then(response => response.json()) 
  .then(data => console.log(data));

It‘s a common pattern, and it works. The browser makes a request to the exact URL you specified. But as your app grows in complexity, especially if you adopt a decoupled architecture with separate front-end and back-end services, those hard-coded absolute paths start to cause some headaches.

In this post, I‘ll explain the problems with absolute API paths and show you a better way using relative paths and reverse proxies. I‘ll walk through how to set it up in a few different environments. By the end, I hope to convince you to (almost) never use an absolute path to your APIs again.

The trouble with absolute paths

To understand the issues with absolute API paths, let‘s consider an example. Imagine you‘re building a web app called MyApp with a React front-end and a Node.js back-end API. In production, you host the front-end at myapp.com and the API at api.myapp.com.

During development, you run the front-end locally at localhost:3000 and the API at localhost:9000. So in your React code, you have something like:

const API_URL = process.env.NODE_ENV === ‘production‘ 
  ? ‘https://api.myapp.com‘
  : ‘http://localhost:9000‘;

// Make API requests
fetch(`${API_URL}/users`)...

This works, but there are a few problems:

  1. CORS conflicts: Since your front-end and API are at different origins, the browser‘s same-origin policy will block requests from the front-end to the API. You‘ll need to configure CORS headers on the API to whitelist the front-end‘s origin.

  2. Multiple environments: With hard-coded URLs, you need a way to specify different API locations for each environment (dev, staging, prod, etc). This makes your build and deploy process more complicated. It‘s harder to follow the 12 Factor App principle of storing config in the environment.

  3. Tight coupling: Your front-end and back-end are tightly coupled by the absolute URLs. If you ever need to change your API location or restructure your URL paths, you‘ll need to update your front-end code (and potentially redeploy).

  4. Insecure and inflexible: Exposing your API‘s hostname in the front-end code makes it easier for attackers to target your API directly. It‘s also harder to do things like load balancing, database failover, etc. since you‘d need to update the hard-coded URL.

While these issues can be mitigated, wouldn‘t it be nice if there was a simpler way? Some way to keep your front-end and API in sync, avoid CORS headaches, and not leak implementation details? There is! And it‘s probably simpler than you think.

A better way with relative paths and reverse proxies

The solution is to use relative paths for your API requests and set up a reverse proxy to forward those requests to your actual API location. So instead of hard-coding https://api.myapp.com/users, just use /api/users and let a proxy handle the rest.

This has a few key benefits:

  1. No more CORS: Since the front-end and API are now on the same origin, there‘s no need for CORS headers.

  2. Environment agnostic: Your front-end code doesn‘t know or care where the actual API server is located. This keeps your codebase clean and portable across environments.

  3. Implementation hiding: Attackers can‘t see your API‘s real hostname. You‘re free to change your API location or infrastructure without updating your front-end.

  4. Simpler deploys: With relative URLs, you can follow 12 Factor and store your API location in environment variables. This makes your build and deploy process much simpler.

In short, proxying through a relative path decouples your front-end from the implementation details of your API. It‘s like an API gateway for your front-end. And you can set it up with a fairly small amount of configuration.

Let‘s see how it works in a few different environments, starting with local development.

Setting up a reverse proxy in development

To proxy API requests in local development, we can use the handy proxying feature built into webpack-dev-server. Many front-end frameworks like Create React App and Vue CLI use webpack-dev-server under the hood, so this will work for most projects.

To set it up, we just need to add a proxy field to our webpack config:

// webpack.config.js
module.exports = {
  // ...
  devServer: {
    proxy: {
      ‘/api‘: ‘http://localhost:9000‘
    }
  }
};

This tells webpack-dev-server to forward any requests to /api to http://localhost:9000 (or wherever your API is running).

So now, instead of referencing http://localhost:9000 directly in your front-end code, you can just use a relative path like /api/users. The dev server will intercept the request and send it to the correct place.

With this proxy in place, you can keep your front-end and API completely separate in development, but still make requests as if they were part of the same app. And you‘re free to restructure your API URLs without ever touching your front-end code.

Proxying in production with NGINX

Of course, webpack-dev-server is just for local development. For production, we need to set up a reverse proxy on our real web server. The most common way to do this is with NGINX.

Setting up an NGINX proxy is fairly straightforward. In your server config file, you just need to proxy_pass requests to the specified path:

# /etc/nginx/conf.d/myapp.conf
server {
  listen 80;
  server_name myapp.com;

  location /api {
    proxy_pass http://api.myapp.com;
  }
}

This tells NGINX to forward any request to /api to the upstream http://api.myapp.com server. So a request from the browser to myapp.com/api/users will get proxied to api.myapp.com/users.

With this config in place, you can deploy your front-end and API to separate servers, but from the browser‘s perspective, they‘ll appear to be part of the same origin. No more CORs errors!

Serverless proxying with CloudFront

So far we‘ve looked at proxying with a traditional web server setup. But what if you‘re using a serverless approach? Sererverless services like AWS Lambda and API Gateway run your back-end code on-demand, without a permanent server.

Fortunately, we can still set up a proxy in the serverless world using AWS CloudFront. CloudFront is a content delivery network (CDN) that can also act as a reverse proxy.

With CloudFront, you create a "distribution" that specifies where to serve content from. You can configure multiple "origins" for a distribution, each with its own path pattern. When a request comes in, CloudFront matches the path and sends the request to the appropriate origin.

So to proxy requests to a serverless API, you‘d create a CloudFront distribution with two origins:

  1. Your front-end S3 bucket
  2. Your API Gateway endpoint

You can use a path pattern like /api/* to send all requests starting with /api to the API Gateway origin. All other requests will go to the S3 bucket.

Here‘s what the CloudFront config might look like:

With this setup, you can make relative API requests from your front-end code (served from S3), and CloudFront will automatically route those requests to API Gateway. No more hard-coded URLs!

The nice thing about this approach is that your API Gateway can be in a totally separate AWS account from your front-end hosting. CloudFront acts as an API gateway for your front-end, proxying requests to the correct back-end service.

Caveats and limitations

While reverse proxies are a great solution for most use cases, there are a few caveats to be aware of.

First, proxying can add some latency to your API requests since they have to go through an additional hop. In practice, this is rarely an issue, especially if you‘re already using a CDN. But it‘s something to consider for extremely latency-sensitive applications.

Second, you need to be careful about the headers and cookies you pass through the proxy. By default, NGINX passes most headers through unchanged, but you may need to manually whitelist some headers for things like authentication.

For cookies, you likely want to set the proxy_cookie_path directive so that cookies set by the API are passed back to the client:

location /api {
  proxy_pass http://api.myapp.com;
  proxy_cookie_path / /api;
}

This rewrites the path in Set-Cookie headers so that cookies set by the API are available to the front-end JavaScript code. See the NGINX docs for more details.

Finally, you need to make sure your API server is configured to accept requests from the proxy. This usually just means setting the correct Host header so that the API server knows how to construct URLs in its responses:

location /api {
  proxy_pass http://api.myapp.com;
  proxy_set_header Host $host;
}

With those caveats in mind, reverse proxies are still a great solution for most web apps. The benefits of simpler code and more flexible infrastructure outweigh the downsides for the vast majority of projects.

Conclusion

In summary, using relative paths and reverse proxies for your API requests has several big advantages over hard-coding absolute URLs:

  • Avoid CORS issues by serving the front-end and API from the same origin
  • Simplify your codebase by keeping URLs relative and environment-agnostic
  • Hide your API implementation details from public view
  • Deploy your front-end and API separately while keeping them in sync

While there are a few things to watch out for, the reverse proxy approach is a big improvement over manually constructing URLs. It‘s a bit more work to set up, but the long-term benefits are worth it.

So the next time you‘re building a web app, try proxying your API requests instead of hard-coding them. It‘ll make your life easier in the long run.

At Myapp, this is the approach we use for all of our projects. It‘s helped us keep our front-end and API codebases clean and separate while still providing a seamless experience for our users. Hopefully it will help you too!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *