How to build Spring Boot multi-client architecture for web and mobile
Purpose
This post demonstrates how to build a single Spring Boot application that serves both web browsers and mobile clients from the same backend.
Environment
- Spring Boot 3.x
- Java 21
- Spring MVC
- Maven/Gradle
The Problem
When I started developing applications that needed to support both web browsers and mobile clients, I got this problem:
How do I serve HTML views for browsers and JSON responses for mobile apps from the same Spring Boot application?I found many developers overcomplicate this by creating separate Spring Boot applications or forcing everything through one controller that returns different response types based on headers.
What I tried first
I tried putting everything in one controller and checking headers to determine the response format:
@RestController@RequestMapping("/users")public class UserController {
@Autowired private UserService userService;
@GetMapping public Object getUsers(@RequestHeader("Accept") String acceptHeader) { List<User> users = userService.getAllUsers();
if (acceptHeader.contains("text/html")) { // This doesn't work - @RestController doesn't return HTML ModelAndView mav = new ModelAndView("users/list"); mav.addObject("users", users); return mav; }
return users; }}But when I run this, I got this error:
java.lang.IllegalStateException: ModelAndView requires a ViewResolverThe problem is @RestController doesn’t know how to handle ModelAndView objects. I need to use the right annotation for each client type.
The Solution: Separate Controllers
I discovered the right approach is using separate controllers for each client type:
// Web Controller for HTML responses@Controller@RequestMapping("/users")public class UserWebController {
@Autowired private UserService userService;
@GetMapping public String listUsers(Model model) { model.addAttribute("users", userService.getAllUsers()); return "users/list"; // Thymeleaf template }
@GetMapping("/{id}") public String getUser(@PathVariable Long id, Model model) { model.addAttribute("user", userService.getUserById(id)); return "users/detail"; }}
// API Controller for JSON responses@RestController@RequestMapping("/api/users")public class UserApiController {
@Autowired private UserService userService;
@GetMapping public List<User> getAllUsers() { return userService.getAllUsers(); }
@GetMapping("/{id}") public ResponseEntity<User> getUser(@PathVariable Long id) { return userService.getUserById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); }}Now test again:
# For web browser (Accept: text/html)GET //usersReturns: users/list.html template with user data
# For mobile app (Accept: application/json)GET /api/usersReturns: [{"id": 1, "name": "User 1"}, ...]You can see that I succeeded to serve both client types properly.
The Shared Service Layer
I found the key is keeping business logic separate from presentation logic:
@Service@Transactionalpublic class UserService {
@Autowired private UserRepository userRepository;
public List<User> getAllUsers() { return userRepository.findAll(); }
public Optional<User> getUserById(Long id) { return userRepository.findById(id); }
// Common business logic public User createUser(UserDTO userDTO) { User user = new User(); user.setName(userDTO.getName()); user.setEmail(userDTO.getEmail()); // Additional validation and business logic return userRepository.save(user); }}This service layer is shared between both controllers, ensuring consistency.
CORS Configuration for Mobile
When I tested my mobile app calls, I got CORS errors:
Access to XMLHttpRequest at 'http://localhost:8080/api/users'from origin 'https://mobile-app.com' has been blocked by CORS policy.I fixed this by adding CORS configuration:
@Configurationpublic class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://mobile-app.com", "http://localhost:3000") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true); }}Now my mobile app calls work without errors.
Why This Works
I think the key reason this architecture works is:
- Clear separation:
@Controllerhandles HTML views,@RestControllerhandles JSON responses - Shared logic: Service classes contain business logic used by both controller types
- Spring’s built-in support: Spring automatically handles content negotiation based on annotations
- Maintainability: Each controller type stays focused on its responsibility
Common Mistakes I Made
I tried using @RequestMapping with produces attribute on the same controller:
// This doesn't work well@RequestMapping(value = "/users", produces = {"application/json", "text/html"})public Object getUsers(...) { // Complex logic to determine response type}This creates messy code. It’s better to have separate controllers.
Another mistake I made was not implementing proper error handling:
// Bad: No error handling@GetMapping("/{id}")public User getUser(@PathVariable Long id) { return userService.getUserById(id).get(); // Throws exception if not found}The correct approach is using ResponseEntity:
@GetMapping("/{id}")public ResponseEntity<User> getUser(@PathVariable Long id) { return userService.getUserById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build());}The Configuration
Here’s my complete project structure:
src/main/java/com/example/demo/├── controller/│ ├── web/│ │ ├── UserWebController.java│ │ └── ProductWebController.java│ └── api/│ ├── UserApiController.java│ └── ProductApiController.java├── service/│ ├── UserService.java│ └── ProductService.java└── config/ └── WebConfig.java
src/main/resources/├── templates/│ ├── users/│ │ ├── list.html│ │ └── detail.html│ └── products/└── static/Summary
In this post, I showed how to build a Spring Boot application that serves both web and mobile clients. The key point is using separate controller layers (@Controller for HTML, @RestController for JSON) while sharing business logic through service classes. This approach keeps your code clean, maintainable, and scalable for multiple client types.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Spring Boot Official Documentation
- 👨💻 Spring MVC Content Negotiation
- 👨💻 CORS Configuration in Spring
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments