The Journey of Breaking Through CORS and Proxy in Vue 3 + Vite + Spring Boot
When building an app combining Vue 3 + Vite on the frontend with Spring Boot on the backend, CORS is the first wall encountered.
This article summarizes what was done to resolve CORS and proxy settings when combining Vue 3 + Vite (frontend) and Spring Boot (backend).
What is CORS?
CORS (Cross-Origin Resource Sharing) is a browser security mechanism. When a frontend running at http://localhost:5173 tries to make a request to a backend running at http://localhost:8080, the browser blocks it as a cross-origin request.
To allow this, the backend must return an Access-Control-Allow-Origin header in the response.
The Two Approaches
There are two main approaches for handling CORS in Vue 3 + Vite + Spring Boot development:
- Development: Proxy through Vite dev server → In development, Vite acts as a proxy and the backend looks like the same origin
- Production: Set CORS headers on Spring Boot → In production, explicitly allow requests from the deployed frontend URL
Let’s look at each in order.
Development: Vite devProxy Settings
During development, configure a proxy in vite.config.ts to have Vite forward API requests to the backend.
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true,
},
},
},
});
With this setting, requests to /api/** from the frontend go to http://localhost:8080/api/**. From the browser’s perspective, since all requests go to localhost:5173, there’s no cross-origin issue.
Spring Boot CORS Settings
In production, explicit CORS settings are needed on the Spring Boot side.
Global Configuration with WebMvcConfigurer
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://your-production-domain.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
Per-Controller Configuration with @CrossOrigin
@RestController
@CrossOrigin(origins = "https://your-production-domain.com")
@RequestMapping("/api/films")
public class FilmController {
// ...
}
For apps where all APIs need CORS, global configuration with WebMvcConfigurer is cleaner.
Preflight OPTIONS Handling
In CORS, before certain requests (those with custom headers or methods other than GET/POST), the browser first sends an OPTIONS request (preflight request) to check whether the actual request is allowed.
If the backend doesn’t respond properly to this OPTIONS, even though GET/POST work, requests with authentication headers will fail.
With Spring Boot’s WebMvcConfigurer CORS configuration, OPTIONS responses are automatically handled. However, if Spring Security is being used, it’s important to also add the CORS setting there.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
// ... other settings
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("https://your-production-domain.com"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
return new UrlBasedCorsConfigurationSource() {{
registerCorsConfiguration("/api/**", configuration);
}};
}
}
allowedOrigins(”*”) and allowCredentials(true) Cannot Coexist
This is a critical point that’s easy to get wrong.
When allowCredentials(true) is set, wildcard (*) cannot be used in allowedOrigins.
// ❌ This doesn't work
registry.addMapping("/api/**")
.allowedOrigins("*") // Wildcard
.allowCredentials(true); // And allowCredentials → Error
// ✅ This is the correct way
registry.addMapping("/api/**")
.allowedOrigins("https://your-domain.com") // Specific URL
.allowCredentials(true);
The error message when this happens is When allowCredentials is true, allowedOrigins cannot contain the special value "*". When you see this, verify that a specific URL is specified in allowedOrigins.
Trailing Slash Trap in Production URLs
When specifying production URLs in CORS settings, watch out for trailing slashes.
// ❌ Be careful with trailing slash
.allowedOrigins("https://your-domain.com/") // Trailing slash is a different URL
// ✅ Without trailing slash
.allowedOrigins("https://your-domain.com")
The URL https://your-domain.com and https://your-domain.com/ are recognized as different origins in the browser. Even though it looks like a minor difference, it can be a cause of “works sometimes, fails sometimes.”
Environment Variable Switching
For managing production URLs in CORS settings, it’s convenient to use environment variables.
Spring Boot: Read via @Value
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${app.cors.allowed-origins}")
private String allowedOrigins;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(allowedOrigins.split(","))
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true);
}
}
# application-local.yml
app:
cors:
allowed-origins: http://localhost:5173
# application-aws.yml
app:
cors:
allowed-origins: https://your-production-domain.com
Frontend: Manage with Vite Environment Variables
// Use VITE_ prefix to expose in browser
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
# .env.development
VITE_API_BASE_URL=http://localhost:8080
# .env.production
VITE_API_BASE_URL=https://your-production-api.com
Summary of Points to Confirm
When debugging CORS, the following points are the main checkboxes:
-
Is
Access-Control-Allow-Originset correctly on backend response? → Verify with browser DevTools → Network tab → response headers -
Is OPTIONS response being returned? → Check if the preflight request (OPTIONS) is also in the Network tab
-
allowedOrigins(”*”) + allowCredentials(true)? → Using both simultaneously is not possible
-
Trailing slash in URL? →
domain.comanddomain.com/are different -
If Spring Security is being used, is CORS setting added to Security side? → WebMvcConfigurer alone is insufficient if Security is involved
-
Is Vite proxy working in development? → Check if requests are going to
localhost:8080via the Network tab
Summary
The points where CORS tends to get stuck in Vue 3 + Vite + Spring Boot are:
- Development: Use Vite devProxy, production: configure backend CORS separately
- When using allowCredentials, specify specific origin (wildcard NG)
- Mind the trailing slash in production URLs
- If using Spring Security, add CORS to the Security side too
Once you understand these points, CORS errors become something you can trace and resolve. Start by checking network requests in browser DevTools.