DEV Community

Lucas Lachance
Lucas Lachance

Posted on • Edited on

Fixing CORS in your SPA

An extremely common issue that plagues new web developers trying to establish communication between their front-end project and a backend or API, is the almighty CORS error. You know what I'm talking about, it's the one that looks like this:

Access to fetch at 'http://localhost:3000/' from origin 'http://localhost:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

What others say

When doing research on this, you'll probably find a lot of different potential solutions, including adding "credentials": "include" in your request (WRONG!), "mode": "no-cors" (WRONG AGAIN!), or having to modify your backend to add CORS headers such as app.use(cors()) in express (functional, but not always possible or ideal).

I'm here to tell you there's a better way. An easier way. One that will make a lot of sense for a lot of people, especially those getting started on simple projects.

Not an absolute solution

I want to clarify that I'm here to address a very particular situation that does not apply to every single case out there. This only applies if you are:

Using a front-end javascript-based Single-Page-Application (SPA), such as: React, Vue, Svelte, SolidJS, Angular, Astro, or any purely frontend SPA, OR you are using vanilla javascript but with a development server like Vite.

That is it. I will explain examples using both Vite as well as the (very deprecated and dead) create-react-app, but the concept should apply to any frontend framework with a development server.

Note: full-stack frameworks such as Next, Nuxt, Solid-Start, etc, won't need this unless you're accessing a secondary backend, but this should be done via the actual backend provided by that framework so is beyond the scope of this article.

Why the error happens

Some optional background information, you can skip this if you only want a solution.

The TL;DR of the CORS error is that a backend server made for a frontend can (and should) only allow for http queries made by a known frontend from a known URL. That is to say, if I'm the owner of my-domain-name.com , I have a backend that's running on my-domain-name.com/api or api.my-domain-name.com , I want to only accept requests made from my-domain-name.com. This prevents any request from other domain names.

To explain a very clear, unambiguous example: Let's say you're bankofamerica.com. You do not want someone to register bankofamerlca.com and be able to show a proper login screen, where people would enter their real Bank of America login and password, check them against your backend, before stealing them... right? That's what CORS is mostly for.

It's also why CORS errors don't affect requests made from another server, or through a proxy : because it's meant to protect a normal end user at home, with no technical know-how, from being a victim of very simple phishing attempts. That can still happen, of course, but it's part of that ongoing battle against scammers and thieves.

But, you might actually want to allow for requests to be made from more than one origin (if you own more than one domain, for example) or from any origin (if you have a public-facing API or service anyone can access). That's why CORS is configurable via the Access-Control-Allow-Origin header, and why adding app.use(cors()) in express works, for example.

But enough background, let's go to the solution.

The solution, in development

Statistically speaking, you're probably doing your development in React, so let's address the most popular option first.

React developers will be using one of the two following development environments.

Create-React-App

If you are using create-react-app, consider moving to Vite as soon as you can, since CRA is deprecated. However, here's the solution anyways because I'm not your mother and I don't make your choices.

Source documentation

To add a proxy, you can simply open your package.json and add the following line at the top level of the json structure:

"proxy": "http://localhost:3000",

You must obviously change the localhost address to your backend URL. You also need to modify all your HTTP requests made with fetch() or axios to start with /api.

For example if you were previously calling http://localhost:3000/login , you would change that to /api/login. The backend would then need to respond do the /login route in this case.

Advanced Configuration needs to be done via the src/setupProxy.js file but should not be required in this case. See more documentation on the subject.

Vite

Source Documentation

A number of frontend frameworks now rely on Vite to provide a fast and efficient development environment with hot module reload (that means, updating the page when you save a file!). Whether it's React, Vue, Svelte, Solid, or even vanilla javascript, you'll find a boilerplate available by running npm create vite@latest and choosing your framework and other options.

Once you have your Vite project running, setting up the proxy is as easy as updating vite.config.js to add the proxy configuration, as so:

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': 'http://localhost:3000',
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

You only need the proxy line in the server object here - the rest of your configuration does not need to match the above. Note that in the default case, requests made to the backend will be prepended with /api, meaning if you call /api/login , the backend call that will be made will be on http://localhost:3000/api/login.

So that means all your backend calls go from something like fetch("http://localhost:3000/endpoint") to fetch("/api/endpoint").

Advanced Configuration is necessary to remove the /api prefix in the calls

You can use a slightly more advanced configuration on Vite to remove the /api prefix:

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

As you can see, it's not that hard, just a simple regex replace.

But what about PRODUCTION???

Some of you might be wondering what happens when you push your code to production, as in, when you deploy it to your hosting. There are obviously a large number of possible situations that might arise, but let me cover a few. Before that, I want to clear out a misconception.

Your proxy configuration and development server are not relevant to production or deployment to a hosting service. That is to say, your development server is just that, development.

The important thing to understand is that invariable in SPAs, to create your production build, you must run a command like npm run build, which creates a new folder in your project called build/ or output/. This will contain a small number of files in html, js, and css format, and that is your static production output. From here, this is where the solutions stem from.

Solution 1: Backend Serves Static Files

The very first, simplest, and probably most valid solution for you, is to serve the build folder files directly from your existing backend server. This obviously only relates if you're the one making the backend, and it's why I noted that using app.use(cors()) wasn't the best way... Because if you simply serve your static files from the backend, you can skip the whole CORS problem in production too!

For example, if you have an express.js backend, you could simply copy over your build folder into the express project, and use the following line:

app.use(express.static('build'))

// your other routes here!
app.get('/api/login', (req, res) => {});
// etc
Enter fullscreen mode Exit fullscreen mode

Yes, that would work perfectly well. It does imply you copy over the files all the time however. The express docs indicate you can also call another folder but it's a bit more complex, so I won't go into that.

This solution works with koa, fastify, hono, and probably any other nodejs backend, they each have an equivalent to express.static(). If you're not in nodejs, look up your docs on how to do it in your prefered backend.

Solution 2: An HTTP Server Proxy

Let's say you don't want to use the backend to serve static files. You can reproduce a similar proxy configuration from your development, into a production http server. This is possible in Apache, Nginx, IIS, or any other http server that has this proxy capability.

For example, an nginx.conf would look like this for static files + express:

server {
  listen 80;
  server_name example.com;
  location / {
    root /path/to/build/folder;
    index index.html;
  }
  location /api {
    proxy_pass http://localhost:3000;
  }
}
Enter fullscreen mode Exit fullscreen mode

Here's another quick example with Apache (The ProxyPass line is all you would need) :

<VirtualHost *:80>
        ServerName example.com
        DocumentRoot /var/www/backup
        ProxyPass /api http://localhost:3000/api/
        <Directory /var/www/backup>
                Require all granted
                Options +Indexes
                AllowOverride None
                Order allow,deny
                Allow from all
        </Directory>
</VirtualHost>
Enter fullscreen mode Exit fullscreen mode

Note that the proxy address above doesn't need to be on the same machine - this can work even if you're hosting the frontend and backend on completely different services!

Solution 3: Actually using CORS

So what if you actually need to have a separate frontend and backend hosted on a completely different domain name, and you can't use a reverse proxy on your web server, and you have absolutely no way to do this any other method than API calls being on a different origin? THAT is when you need to think about CORS.

The very basics of CORS is extremely simple. You just need to add a header to any outgoing request sent from your backend server. The frontend cannot control this, it has to be a backend response header, period.

The header will look like Access-Control-Allow-Origin: https://example.com for a specific domain name, Access-Control-Allow-Origin: http://somedomain.com:8080 for, say, something on a different port, or if you really want to let anyone access your service, Access-Control-Allow-Origin: *.

It's not possible to add more than one ACAO header, or add more than one domain to the header. In order to support multiple origin domains, you would need backend logic that modifies this header to allow for the one doing the request. Most CORS libraries for your backend should have that capability, but obviously it's beyond the scope of this tutorial.

Conclusion

That's all, folks! Hopefully this extensive and verbose (sorry!) post is enough for you to understand why CORS errors happen, how to resolve them while you're developing your website, and even how to deal with them in production. If you have any questions, please don't hesitate to ask below and I'll try my best to help! 💖

Top comments (0)