REST API in Spring Boot: The Journey from Request to Response

21-01-2026

By Mario S. Pinto Miranda

Imagine you are a new developer in Spring Boot. You write these few lines of code and, as if by magic, you have a fully functional REST API:

@SpringBootApplication public class MiAplicacion { public static void main(String[] args) { SpringApplication.run(MiAplicacion.class, args); } } @RestController public class UsuarioController { @GetMapping("/api/usuarios") public List<Usuario> obtenerUsuarios() { return Arrays.asList(new Usuario("Juan"), new Usuario("María")); } }

You run mvn spring-boot:run, open your browser at http://localhost:8080/api/usuarios, and you get a perfectly formatted JSON response. But how?

How Does Spring Boot Find Your Method?

This is the fundamental question that will accompany us throughout this journey: when a web client makes an HTTP GET request to /api/usuarios, how is it possible that the obtenerUsuarios() method is specifically executed among all the classes and methods in your project?

To answer this, we need to inspect what happens in Spring Boot. If we analyze it, we will see two clearly differentiated phases:

  1. Startup phase: Where the entire infrastructure is built
  2. Runtime phase: Where requests are processed in real-time

Let's look at each phase in detail.

Phase 1: Startup - Building the Infrastructure

Everything begins with that seemingly simple @SpringBootApplication annotation in your main class. However, @SpringBootApplication is much more than it seems, as it is a composite annotation that includes three fundamental elements:

@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan public @interface SpringBootApplication { // The true composition behind the "magic" }

Each of these annotations has a specific purpose in building your application:

@ComponentScan

@ComponentScan enables component scanning (@Component) in the package where the application is located. When Spring Boot starts, it executes a meticulous process:

src/main/java/ └── com.miempresa.miapp/ ├── MiAplicacion.java (@SpringBootApplication) <- Starting point ├── controllers/ │ └── UsuarioController.java (@RestController) <- Found! ├── services/ └── repositories/

The algorithm is elegantly simple:

  1. Takes the package where your main class is (com.miempresa.miapp)
  2. Recursively scans all sub-packages
  3. Looks for classes annotated with @RestController, @Controller, @Service, @Repository, etc.
  4. Registers them as "beans" in the Spring container

Critical implication for your code: If you place a @RestController outside this package tree, Spring Boot will not find it automatically. It's not magic: it's scanning logic.

@EnableAutoConfiguration

Here is the heart of Spring Boot's "magic." @EnableAutoConfiguration tells Spring Boot to start adding beans based on classpath configuration, other beans, and various property configurations.

When you add this dependency to your pom.xml:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>

Spring Boot automatically detects the available libraries and configures:

  • An embedded web server (Tomcat by default)
  • The DispatcherServlet to handle HTTP requests
  • JSON converters (Jackson)
  • Customizable error handling
  • And several more configurations…

Auto-configuration works through an elegant pattern using @Conditional annotations:

@AutoConfiguration @ConditionalOnClass(ObjectMapper.class) // Is Jackson on the classpath? @ConditionalOnMissingBean(ObjectMapper.class) // Did the developer NOT configure one themselves? public class JacksonAutoConfiguration { @Bean public ObjectMapper objectMapper() { // Spring Boot configures Jackson automatically return new ObjectMapper(); } }

Let's see the logic behind this:

  • If you have Jackson in your classpath (because you included spring-boot-starter-web)
  • And you haven't defined your own ObjectMapper
  • Then Spring Boot configures one automatically with sensible configurations

If you define your own ObjectMapper:

@Bean public ObjectMapper miConfiguracionPersonalizada() { ObjectMapper mapper = new ObjectMapper(); // Your specific configuration return mapper; }

Spring Boot detects your bean and respects your decision, not interfering with your configuration.

Fundamental philosophy: "Convention over Configuration with Escape Hatch" - everything works without configuration, but you can always customize what you need.

Alright, so far we've seen how Spring Boot discovers your controllers and configures the environment. But, how does it know which method to execute for a specific request?

Building the Mappings Index: The Key to Performance

During startup, Spring Boot performs one of its most critical operations: building an efficient index of all your endpoints. This process, which occurs only once, is fundamental for runtime performance.

The step-by-step process:

  1. Discovery of Controllers: Spring Boot examines all beans marked with @RestController that it found during component scanning

  2. Method Analysis: For each controller, it inspects each method looking for mapping annotations (@GetMapping, @PostMapping, etc.)

  3. Building Complete Routes: Combines class and method routes:

    @RequestMapping("/api") // Controller base route @RestController public class UsuarioController { @GetMapping("/usuarios") // Specific method route public List<Usuario> obtener() { ... } } // Final result: GET:/api/usuarios
  4. Creation of Optimized Index: Stores the information in an O(1) access data structure, typically a HashMap where the key is "HTTP_METHOD:URL" and the value contains the reference to the Java method and associated metadata.

Seeing how this works, conflicts may arise if you have duplicate routes. What happens if you have two methods with the same route and HTTP method?

Validation and Conflict Detection

During this construction, Spring Boot also performs critical validations:

// This would cause an error when starting the application @RestController public class UsuarioController { @GetMapping("/usuarios") public List<Usuario> obtener() { ... } @GetMapping("/usuarios") // Duplicate detected! public Usuario obtenerUno() { ... } }

The conflict detection algorithm is immediately effective:

  • When attempting to register the second mapping GET:/usuarios
  • Spring Boot detects that the key already exists in the index
  • The application immediately fails during startup with a clear message
  • There is no ambiguity or undefined behavior at runtime

We already have the infrastructure ready and an optimized index of mappings. Now let's see how a request is handled in real-time.

Phase 2: Runtime - The Flow of a Request in Action

Once startup is complete, each HTTP request that arrives at your application follows an optimized and predictable flow.

The DispatcherServlet: The Orchestra Conductor

Spring MVC is designed around the front controller pattern where a central Servlet, the DispatcherServlet, provides a shared algorithm for request processing, while the actual work is performed by configurable delegate components.

When a GET /api/usuarios request arrives:

  1. Reception by the Web Server: Embedded Tomcat receives the HTTP connection
  2. Delegation to the DispatcherServlet: As the central front controller, it receives all requests
  3. HandlerMapping Query: One of the crucial responsibilities of the DispatcherServlet is to query the HandlerMapping. This mapping is responsible for determining which controller (handler) should process the incoming request based on factors such as the request URL, request method, or other parameters
  4. Index Search: Looks for GET:/api/usuarios in the index built during startup
  5. Metadata Retrieval: Obtains the reference to the UsuarioController.obtenerUsuarios() method and its metadata

How does Spring Boot handle the conversion of JSON to Java objects and vice versa?

Automatic Conversion: Jackson in Action

Before and after executing your business method, Spring Boot automatically handles conversions using MappingJackson2HttpMessageConverter:

For incoming requests:

  • Content-Type: application/json → Java Object using Jackson
  • URL parameters → primitive types or objects

For outgoing responses:

  • Your return object → JSON using Jackson
  • Accept: application/json → automatic serialization
@PostMapping("/usuarios") public Usuario crearUsuario(@RequestBody Usuario usuario) { // Jackson automatically converted JSON → Usuario Usuario guardado = usuarioService.guardar(usuario); // Jackson will automatically convert Usuario → JSON return guardado; }

Jackson: Transparent Automatic Configuration

Spring Boot configures Jackson automatically using the conditional pattern:

@AutoConfiguration @ConditionalOnClass(ObjectMapper.class) @ConditionalOnMissingBean public class JacksonAutoConfiguration { @Bean public ObjectMapper objectMapper() { return new ObjectMapper() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .registerModule(new JavaTimeModule()); } }

Customization respecting auto-configuration:

@Configuration public class JacksonConfig { @Bean @Primary public ObjectMapper customObjectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); return mapper; } }

HandlerMappings Architecture: Modular Design

Spring Boot does not use a single monolithic "mapper," but multiple specialized HandlerMappings that operate in coordination:

  • RequestMappingHandlerMapping: Handles your @RestController and @Controller
  • SimpleUrlHandlerMapping: For static resources (/css/*, /js/*)
  • BeanNameUrlHandlerMapping: For mappings based on bean names
  • RouterFunctionMapping: For reactive functional programming

The RequestMappingHandlerMapping is specifically the component that:

  1. Scans methods with @GetMapping, @PostMapping, etc. annotations
  2. Builds the index in an optimized manner during startup
  3. Resolves requests in O(1) time using an internal HashMap

This modular architecture allows:

  • Extensibility without modifying existing components
  • Performance optimized for each type of mapping
  • Clarity in the separation of responsibilities

Alright, now that we understand the process, how can we inspect it in action?

Tools to Explore the Internals: From Theory to Practice

With Spring Boot, you have several tools to see exactly what is happening under the hood.

Development Logs: Seeing the Process in Action

Add these configurations to your application.properties to observe the entire process we have described:

logging.level.org.springframework.web=DEBUG logging.level.org.springframework.web.servlet.mvc.method.annotation=TRACE

During startup, you will see output similar to:

Mapped "{[/api/usuarios],methods=[GET]}" onto public java.util.List com.ejemplo.UsuarioController.obtenerUsuarios()

These lines confirm exactly the index-building process we have described.

Spring Boot Actuator: Real-Time Inspection

To get a complete view of the registered mappings:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
management.endpoints.web.exposure.include=mappings,beans,conditions

Useful endpoints for introspection:

  • /actuator/mappings: Detailed JSON with all registered mappings, expected parameters, return types, and configuration metadata
  • /actuator/beans: All beans registered in the Spring container
  • /actuator/conditions: Complete auto-configuration report, showing which conditions were evaluated and their results
  • /actuator/configprops: All available configuration properties

Visiting http://localhost:8080/actuator/mappings gives you a detailed JSON with:

  • All registered mappings
  • Information about expected parameters
  • Return types
  • Configuration metadata
  • Automatic mappings that Spring Boot adds internally

Advanced Logs for Debugging

logging.level.org.springframework.boot.autoconfigure=DEBUG logging.level.org.springframework.boot.autoconfigure.condition=TRACE # See specifically the request mapping process logging.level.org.springframework.web=DEBUG logging.level.org.springframework.web.servlet.mvc.method.annotation=TRACE

Practical Implications: How This Knowledge Changes Your Development

Intelligent Package Structure

Correct:

com.miempresa.miapp/ ├── Application.java (@SpringBootApplication) ├── controllers/Will be automatically detected ├── services/Will be automatically detected └── repositories/Will be automatically detected

Problematic:

com.miempresa.miapp/ ├── Application.java (@SpringBootApplication) └── controllers/ com.otraempresa.external/ └── SpecialController.javaWill NOT be automatically detected

Customization Respecting Auto-Configuration

Take advantage of Spring Boot's conditional system:

@Configuration public class MiConfiguracion { @Bean @ConditionalOnMissingBean public ObjectMapper objectMapperPersonalizado() { ObjectMapper mapper = new ObjectMapper(); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); return mapper; } // Will only be used if Spring Boot did not automatically configure one }

Systematic Debugging vs Trial and Error

Before understanding the internals:

  • "My controller doesn't work" → Random trial and error

After understanding the internals:

  • Verify package structure
  • Confirm correct annotations
  • Review startup logs
  • Consult /actuator/mappings
  • Systematic and efficient debugging

Conclusion: The Magic Demystified

Understanding these internal mechanisms transforms you from being a user of Spring Boot to being an informed collaborator. You no longer rely on trial and error - you have a clear mental map of how the system works.

When you encounter problems, you will know:

  • Where to look (logs, actuator, package structure)
  • What to check (annotations, configurations, conflicts)
  • How to customize (leveraging conditional patterns)

The next time you see your Spring Boot application start in seconds and perfectly respond to HTTP requests, you will remember that you did not witness magic - you observed exceptional software engineering.

Spring Boot does not avoid complexity - it manages it elegantly, allowing you to focus on your business logic while handling the infrastructure with solid design principles.

The true "magic" lies in how these components work together harmoniously to create a development experience that feels smooth and natural, but is backed by robust architecture and carefully considered design decisions.

References and Official Documentation

To delve deeper into the concepts explained, consult these official sources:

“Spring MVC, as many other web frameworks, is designed around the front controller pattern where a central Servlet, the DispatcherServlet, provides a shared algorithm for request processing, while actual work is performed by configurable delegate components.”

Official Spring Framework Documentation