Hilla is a full-stack framework for building web applications with Spring Boot and React. It embraces the backends for frontends (BFF) pattern, where backend services are tailored for specific frontends.
Hilla makes it easy to connect Java backends to TypeScript React frontends using type-safe RPC endpoints. You can call Java methods directly from TypeScript without manually defining REST APIs.
In this post, we'll look at consuming microservices with Hilla through a simple e-commerce example application.
Project Overview
Our application has two backend services - a user
service and an order
service. A Hilla application combines data from the services and exposes it to the React frontend.
The Hilla app acts as an aggregator and provides a single endpoint for the frontend. This avoids the frontend having to call multiple services directly.
The Backend Services
The user
and order
services are regular Spring Boot apps. They use JPA and Spring Data to persist entities to a database.
Here are the relevant parts of the services:
User Service
@Entity
public class User {
@Id
private Long id;
private String name;
private String email;
// getters and setters
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {}
@Service
public class UserService {
private final UserRepository userRepository;
// ...
public List<User> getAllUsers() {
return userRepository.findAll();
}
}
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
// ...
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
}
Order Service
@Entity
public class Order {
@Id
private Long id;
private Long userId;
private String product;
private Double price;
// ...
}
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByUserId(Long userId);
}
@Service
public class OrderService {
private final OrderRepository orderRepository;
// ...
public List<Order> getOrdersForUser(Long userId) {
return orderRepository.findByUserId(userId);
}
}
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
// ...
@GetMapping("/user/{userId}")
public List<Order> getOrdersForUser(@PathVariable Long userId) {
return orderService.getOrdersForUser(userId);
}
}
These are standard Spring Boot REST controllers that return JSON responses.
The Hilla Application
The Hilla app calls the user and order services and combines the data into a single type-safe endpoint for the frontend:
@Endpoint
public class UserDetailsService {
public record User(Long id, String name, String email) {}
public record Order(Long id, Long userId, String product, Double price) {}
public record UserDetails(User user, List<Order> orders) {}
public List<UserDetail> getUserDetails() {
// Call user service to get all users
var users = getUsers();
return users.stream()
.map(user -> {
// Call order service to get all orders for each user
var orders = getOrders(user.id());
return new UserDetail(user, orders);
})
.toList();
}
private List<User> getUsers() {
WebClient userClient = webClientBuilder.baseUrl(userServiceUrl).build();
var users = userClient.get()
.uri("/users")
.retrieve()
.bodyToFlux(User.class)
.collectList()
.block();
return users;
}
private List<Order> getOrders(Long userId) {
WebClient orderClient = webClientBuilder.baseUrl(orderServiceUrl).build();
var orders = orderClient.get()
.uri("/orders/user/" + userId)
.retrieve()
.bodyToFlux(Order.class)
.collectList()
.block();
return orders;
}
}
The @Endpoint
annotation exposes getUserDetails
as a public endpoint that's callable from TypeScript.
The frontend can now fetch the aggregated user details in one call from TypeScript:
const userDetails = await UserDetailsService.getUserDetails();
No need to manually call the /users
and /orders
endpoints separately.
The Frontend
Putting it all together, the frontend can fetch user details and display them in a grid based on the selected user:
export default function App() {
const [userDetails, setUserDetails] = useState<UserDetail[]>([]);
const [orders, setOrders] = useState<Order[]>([]);
useEffect(() => {
UserDetailsService.getUserDetails().then(setUserDetails);
}, []);
function selectedUserChanged(
e: ComboBoxSelectedItemChangedEvent<UserDetail>
) {
const orders = e.detail.value ? e.detail.value.orders : [];
setOrders(orders);
}
return (
<div className="flex flex-col items-start gap-l p-m">
<h1>Hilla microservice example</h1>
<ComboBox
label="Select user to view orders"
items={userDetails}
itemLabelPath="user.name"
onSelectedItemChanged={selectedUserChanged}
/>
<Grid items={orders}>
<GridColumn path="product" />
<GridColumn path="price" />
</Grid>
</div>
);
}
Conclusion
Hilla makes it easy to integrate Java microservices with a React frontend using type-safe endpoints. The Hilla app can aggregate data from services and act as a backend for the UI.
This example showed a simple read-only application, but Hilla endpoints also support taking complex parameters and returning domain objects. That makes it easy to build full CRUD functionality across microservices.
Check out the Hilla microservices example to see the full application code. Let us know if you have any other questions!