Custom Base API URLs in Spring Boot & Spring MVC

Configuring a robust base URL (like /api/v1) is a fundamental step in building scalable Java microservices. A well-structured base path ensures seamless API versioning, simplifies routing rules for API Gateways, and prevents endpoint collisions.

Here are the three standard approaches to configuring base URLs in Spring Boot, ranked from the most globally applied to the most granular, along with production best practices.

Approach 1: The Global Configuration Property (Simplest & Most Common)

The easiest way to define a base URL is by utilizing Spring Boot’s externalized configuration. This approach changes the root path for the entire application, including embedded server endpoints.

application.yml (Production Profile)

server:
  servlet:
    context-path: /api/v1

Theory & Nuance:

  • Scope: This shifts the root of your application container (Tomcat, Jetty, Undertow). If you have a controller mapped to /users, the full path becomes http://localhost:8080/api/v1/users.

  • Impact: This also changes the path for Spring Boot Actuator endpoints unless configured otherwise.


1. server.servlet.context-path (The “App” Level)

This property defines the root path for the entire web application. It is a standard Java Servlet specification feature.

  • Layer: Web Container (Tomcat/Jetty).

  • Scope: Everything. This includes your Spring MVC Controllers, your static resources (index.html), and Spring Boot Actuator endpoints.

  • Analogy: The “Folder Name” on the server where your entire app lives.

Example:

If server.servlet.context-path=/myapp, then:

  • Controller at /users → http://localhost:8080/myapp/users

  • Actuator at /health → http://localhost:8080/myapp/actuator/health


2. spring.mvc.servlet.path (The “Spring” Level)

This property defines the mapping for the DispatcherServlet. The DispatcherServlet is just one servlet inside your application that handles Spring MVC requests.

  • Layer: Spring Framework / DispatcherServlet.

  • Scope: Only things handled by Spring MVC (Controllers). It does not affect other servlets or static content handled directly by the container.

  • Analogy: The “Front Door” specifically for your Spring API logic.

Example:

If spring.mvc.servlet.path=/api, then:

  • Controller at /users → http://localhost:8080/api/users

  • Note: If you have a static file src/main/resources/static/index.html, it is still at http://localhost:8080/index.html (because it bypasses the DispatcherServlet).


When a request comes in, it must pass through the Context Path first, then the Servlet Path, and finally the Controller Mapping.

Featureserver.servlet.context-pathspring.mvc.servlet.path
OwnerThe Web Server (Tomcat)Spring MVC (DispatcherServlet)
Affects Static Content?YesNo
Affects Actuator?YesDepends (usually Yes, but can be decoupled)
Typical Use CaseDeploying multiple apps on one server.Versioning or isolating API logic from UI.

Approach 2: Programmatic Global Prefixing (Highly Flexible)

For applications that serve both web pages (MVC) and REST APIs, or if you only want to prefix specific types of controllers, applying a global prefix programmatically is the cleanest approach.

Code Snippet: WebMvcConfigurer Implementation

package com.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        // Applies "/api/v1" prefix strictly to classes annotated with @RestController
        configurer.addPathPrefix("/api/v1",
            HandlerTypePredicate.forAnnotation(RestController.class));
    }
}

Theory & Nuance:

  • Scope: Targeted. In the example above, only controllers annotated with @RestController will receive the /api/v1 prefix. Standard @Controller classes serving UI views remain unaffected.

  • Production Value: Excellent for monolithic applications transitioning to microservices, as it strictly segments API routes from frontend routes.

Approach 3: Controller-Level Mapping (Granular & Explicit)

The most standard Spring MVC approach is applying the base path directly at the controller level using @RequestMapping.

Code Snippet: REST Controller

package com.example.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("${api.base-path:/api/v1}/users") // Uses property with a default fallback
public class UserController {

    @GetMapping
    public String getUsers() {
        return "List of users";
    }
}

Theory & Nuance:

  • Scope: Localized to the specific controller.

  • Production Value: Notice the use of ${api.base-path:/api/v1}. Hardcoding paths in annotations is considered an anti-pattern in large codebases. Resolving the prefix via properties allows you to change the version globally across all controllers by tweaking a single property in your application-prod.yml.


🚀 Production-Ready Best Practices

If asked in an interview how you handle base URLs in a true enterprise production environment, highlight these points:

  1. Environment-Specific Profiles: Never hardcode base URLs. Use Spring Profiles (application-dev.ymlapplication-prod.yml) to manage paths. What runs at /api/v1 locally might need to run at /internal-api/v1 in a staging environment.

  2. API Gateway Routing (The “No-Prefix” Pattern): In modern microservice architectures (using Kubernetes Ingress, AWS API Gateway, or Spring Cloud Gateway), the Spring Boot app itself is often mapped simply to /. The API Gateway handles the /api/v1/users request and strips the /api/v1 prefix before forwarding it to the backend service. This decouples routing logic from the application logic.

  3. Explicit Versioning: Always include a version node (e.g., /v1//v2/) in the URI or configure your application to accept versioning via Request Headers (e.g., Accept: application/vnd.company.app-v1+json). URI versioning is the most cache-friendly and easily debugged approach.

  4. Actuator Security: If using server.servlet.context-path, ensure your health checks and metrics (/actuator/health) are properly secured or mapped to a different port for internal cluster monitoring.

Post a Comment

Previous Post Next Post