BLOG POSTS
    MangoHost Blog / Spring RestTemplate Example – How to Make HTTP Requests
Spring RestTemplate Example – How to Make HTTP Requests

Spring RestTemplate Example – How to Make HTTP Requests

Spring’s RestTemplate has been the go-to solution for making HTTP requests in Spring applications for years, though it’s now in maintenance mode with WebClient being the preferred async alternative. RestTemplate provides a synchronous client-side HTTP access library that simplifies communication with RESTful services through a variety of HTTP methods. You’ll learn how to configure RestTemplate properly, handle different HTTP methods, manage request/response transformations, implement error handling, and apply best practices for production environments.

Understanding RestTemplate Architecture

RestTemplate operates as a synchronous HTTP client built on top of Java’s HttpURLConnection or other HTTP client libraries like Apache HttpClient. It uses a template method pattern where you define the HTTP operation, and RestTemplate handles the underlying connection management, request/response marshalling, and resource cleanup.

The core components include:

  • ClientHttpRequestFactory – Creates HTTP requests
  • HttpMessageConverter – Handles request/response body conversion
  • ResponseErrorHandler – Manages HTTP error responses
  • ClientHttpRequestInterceptor – Provides request/response interception

RestTemplate supports automatic JSON/XML serialization through Jackson or JAXB, making it seamless to work with POJOs instead of raw HTTP data.

Basic RestTemplate Setup and Configuration

First, ensure you have the necessary dependencies in your project. For Maven:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.3.21</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.13.3</version>
</dependency>

Create a basic RestTemplate configuration:

@Configuration
public class RestTemplateConfig {
    
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        
        // Configure timeout settings
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setConnectTimeout(5000);
        factory.setReadTimeout(10000);
        
        restTemplate.setRequestFactory(factory);
        return restTemplate;
    }
    
    @Bean
    public RestTemplate customRestTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        
        // Add custom error handler
        restTemplate.setErrorHandler(new CustomResponseErrorHandler());
        
        // Add interceptors for logging or authentication
        List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
        interceptors.add(new LoggingRequestInterceptor());
        restTemplate.setInterceptors(interceptors);
        
        return restTemplate;
    }
}

HTTP Methods Implementation Examples

Here’s a comprehensive service class demonstrating various HTTP operations:

@Service
public class ApiService {
    
    @Autowired
    private RestTemplate restTemplate;
    
    private static final String BASE_URL = "https://jsonplaceholder.typicode.com";
    
    // GET request with path variables
    public User getUserById(Long id) {
        String url = BASE_URL + "/users/{id}";
        return restTemplate.getForObject(url, User.class, id);
    }
    
    // GET request with query parameters
    public List<Post> getPostsByUserId(Long userId) {
        String url = BASE_URL + "/posts?userId={userId}";
        
        ResponseEntity<Post[]> response = restTemplate.getForEntity(url, Post[].class, userId);
        return Arrays.asList(response.getBody());
    }
    
    // POST request with request body
    public Post createPost(Post post) {
        String url = BASE_URL + "/posts";
        
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        
        HttpEntity<Post> requestEntity = new HttpEntity<>(post, headers);
        return restTemplate.postForObject(url, requestEntity, Post.class);
    }
    
    // PUT request for updates
    public void updatePost(Long id, Post post) {
        String url = BASE_URL + "/posts/{id}";
        
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        
        HttpEntity<Post> requestEntity = new HttpEntity<>(post, headers);
        restTemplate.put(url, requestEntity, id);
    }
    
    // DELETE request
    public void deletePost(Long id) {
        String url = BASE_URL + "/posts/{id}";
        restTemplate.delete(url, id);
    }
    
    // Generic exchange method for complex scenarios
    public ResponseEntity<String> makeCustomRequest() {
        String url = BASE_URL + "/posts/1";
        
        HttpHeaders headers = new HttpHeaders();
        headers.add("Custom-Header", "CustomValue");
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        
        HttpEntity<String> entity = new HttpEntity<>(headers);
        
        return restTemplate.exchange(
            url,
            HttpMethod.GET,
            entity,
            String.class
        );
    }
}

Supporting POJO classes:

public class User {
    private Long id;
    private String name;
    private String email;
    private String phone;
    
    // constructors, getters, setters
}

public class Post {
    private Long id;
    private Long userId;
    private String title;
    private String body;
    
    // constructors, getters, setters
}

Advanced Configuration and Error Handling

Implement robust error handling and custom configurations:

public class CustomResponseErrorHandler implements ResponseErrorHandler {
    
    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        return response.getStatusCode().series() == HttpStatus.Series.CLIENT_ERROR ||
               response.getStatusCode().series() == HttpStatus.Series.SERVER_ERROR;
    }
    
    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
        HttpStatus statusCode = response.getStatusCode();
        
        switch (statusCode.series()) {
            case CLIENT_ERROR:
                if (statusCode == HttpStatus.NOT_FOUND) {
                    throw new ResourceNotFoundException("Resource not found");
                } else if (statusCode == HttpStatus.BAD_REQUEST) {
                    throw new BadRequestException("Bad request: " + getResponseBody(response));
                }
                break;
            case SERVER_ERROR:
                throw new ServerException("Server error: " + statusCode);
            default:
                throw new RestClientException("Unknown error: " + statusCode);
        }
    }
    
    private String getResponseBody(ClientHttpResponse response) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.getBody()))) {
            return reader.lines().collect(Collectors.joining("\n"));
        }
    }
}

public class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {
    
    private static final Logger logger = LoggerFactory.getLogger(LoggingRequestInterceptor.class);
    
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, 
                                      ClientHttpRequestExecution execution) throws IOException {
        
        logger.info("Request URI: {}", request.getURI());
        logger.info("Request Method: {}", request.getMethod());
        logger.info("Request Headers: {}", request.getHeaders());
        
        if (body.length > 0) {
            logger.info("Request Body: {}", new String(body, StandardCharsets.UTF_8));
        }
        
        ClientHttpResponse response = execution.execute(request, body);
        
        logger.info("Response Status: {}", response.getStatusCode());
        logger.info("Response Headers: {}", response.getHeaders());
        
        return response;
    }
}

Authentication and Security Implementation

Handle various authentication schemes:

@Service
public class SecureApiService {
    
    @Autowired
    private RestTemplate restTemplate;
    
    // Basic Authentication
    public String callSecureEndpointBasicAuth(String username, String password) {
        String url = "https://api.example.com/secure-data";
        
        HttpHeaders headers = new HttpHeaders();
        String auth = username + ":" + password;
        byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes(StandardCharsets.UTF_8));
        String authHeader = "Basic " + new String(encodedAuth);
        headers.set("Authorization", authHeader);
        
        HttpEntity<String> entity = new HttpEntity<>(headers);
        
        ResponseEntity<String> response = restTemplate.exchange(
            url, HttpMethod.GET, entity, String.class);
        
        return response.getBody();
    }
    
    // Bearer Token Authentication
    public ApiResponse callWithBearerToken(String token, Object requestData) {
        String url = "https://api.example.com/protected-resource";
        
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(token);
        headers.setContentType(MediaType.APPLICATION_JSON);
        
        HttpEntity<Object> entity = new HttpEntity<>(requestData, headers);
        
        return restTemplate.postForObject(url, entity, ApiResponse.class);
    }
    
    // Custom header authentication
    public void callWithApiKey(String apiKey) {
        String url = "https://api.example.com/data";
        
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-API-Key", apiKey);
        headers.add("X-Client-Version", "1.0");
        
        HttpEntity<String> entity = new HttpEntity<>(headers);
        
        restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
    }
}

Performance Optimization and Connection Pooling

Configure Apache HttpClient for better performance:

@Configuration
public class HighPerformanceRestTemplateConfig {
    
    @Bean
    public RestTemplate highPerformanceRestTemplate() {
        
        // Configure connection pooling
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(100);
        connectionManager.setDefaultMaxPerRoute(20);
        
        // Configure timeouts and retry policy
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(5000)
                .setConnectTimeout(5000)
                .setSocketTimeout(10000)
                .build();
        
        // Build HttpClient with custom configuration
        CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig)
                .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true))
                .build();
        
        // Create RestTemplate with HttpClient
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
        
        RestTemplate restTemplate = new RestTemplate(factory);
        
        // Add compression support
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
        messageConverters.add(new StringHttpMessageConverter());
        messageConverters.add(new MappingJackson2HttpMessageConverter());
        restTemplate.setMessageConverters(messageConverters);
        
        return restTemplate;
    }
}

RestTemplate vs Alternatives Comparison

Feature RestTemplate WebClient OkHttp Apache HttpClient
Execution Model Synchronous Asynchronous/Reactive Sync/Async Synchronous
Spring Integration Native Native (Spring 5+) Third-party Third-party
Performance Good Excellent Excellent Good
Memory Usage Medium Low Low Medium
Learning Curve Easy Moderate Easy Moderate
Maintenance Status Maintenance Mode Active Development Active Active

Real-World Use Cases and Integration Patterns

Implement a service that integrates with multiple external APIs:

@Service
public class IntegrationService {
    
    @Autowired
    private RestTemplate restTemplate;
    
    @Value("${payment.api.url}")
    private String paymentApiUrl;
    
    @Value("${notification.api.url}")
    private String notificationApiUrl;
    
    @Retryable(value = {ResourceAccessException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000))
    public PaymentResponse processPayment(PaymentRequest request) {
        String url = paymentApiUrl + "/process";
        
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setBearerAuth(getPaymentApiToken());
        
        HttpEntity<PaymentRequest> entity = new HttpEntity<>(request, headers);
        
        try {
            ResponseEntity<PaymentResponse> response = restTemplate.exchange(
                url, HttpMethod.POST, entity, PaymentResponse.class);
            
            // Log successful payment
            auditPayment(request, response.getBody());
            
            return response.getBody();
            
        } catch (HttpClientErrorException e) {
            if (e.getStatusCode() == HttpStatus.BAD_REQUEST) {
                throw new InvalidPaymentException("Invalid payment data: " + e.getResponseBodyAsString());
            }
            throw new PaymentProcessingException("Payment failed: " + e.getMessage());
        }
    }
    
    @Async
    public CompletableFuture<Void> sendNotificationAsync(NotificationRequest notification) {
        return CompletableFuture.runAsync(() -> {
            String url = notificationApiUrl + "/send";
            
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.add("X-API-Key", getNotificationApiKey());
            
            HttpEntity<NotificationRequest> entity = new HttpEntity<>(notification, headers);
            
            try {
                restTemplate.postForEntity(url, entity, String.class);
            } catch (Exception e) {
                // Log error but don't fail the main process
                log.error("Failed to send notification: {}", e.getMessage());
            }
        });
    }
    
    // Implement circuit breaker pattern
    @CircuitBreaker(name = "external-service", fallbackMethod = "fallbackGetData")
    public ExternalData getExternalData(String id) {
        String url = "https://external-api.com/data/{id}";
        return restTemplate.getForObject(url, ExternalData.class, id);
    }
    
    public ExternalData fallbackGetData(String id, Exception ex) {
        // Return cached data or default response
        return getCachedData(id).orElse(new ExternalData("default", "Fallback data"));
    }
}

Testing RestTemplate Interactions

Comprehensive testing strategies for RestTemplate-based services:

@ExtendWith(MockitoExtension.class)
class ApiServiceTest {
    
    @Mock
    private RestTemplate restTemplate;
    
    @InjectMocks
    private ApiService apiService;
    
    @Test
    void testGetUserById() {
        // Given
        Long userId = 1L;
        User expectedUser = new User(userId, "John Doe", "john@example.com");
        String expectedUrl = "https://jsonplaceholder.typicode.com/users/{id}";
        
        when(restTemplate.getForObject(expectedUrl, User.class, userId))
            .thenReturn(expectedUser);
        
        // When
        User actualUser = apiService.getUserById(userId);
        
        // Then
        assertThat(actualUser).isNotNull();
        assertThat(actualUser.getName()).isEqualTo("John Doe");
        verify(restTemplate).getForObject(expectedUrl, User.class, userId);
    }
    
    @Test
    void testCreatePost() {
        // Given
        Post inputPost = new Post(null, 1L, "Test Title", "Test Body");
        Post expectedPost = new Post(101L, 1L, "Test Title", "Test Body");
        String expectedUrl = "https://jsonplaceholder.typicode.com/posts";
        
        ArgumentCaptor<HttpEntity<Post>> entityCaptor = ArgumentCaptor.forClass(HttpEntity.class);
        
        when(restTemplate.postForObject(eq(expectedUrl), entityCaptor.capture(), eq(Post.class)))
            .thenReturn(expectedPost);
        
        // When
        Post actualPost = apiService.createPost(inputPost);
        
        // Then
        assertThat(actualPost.getId()).isEqualTo(101L);
        
        HttpEntity<Post> capturedEntity = entityCaptor.getValue();
        assertThat(capturedEntity.getBody()).isEqualTo(inputPost);
        assertThat(capturedEntity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
    }
}

// Integration testing with MockRestServiceServer
@SpringBootTest
class ApiServiceIntegrationTest {
    
    @Autowired
    private ApiService apiService;
    
    @Autowired
    private RestTemplate restTemplate;
    
    private MockRestServiceServer mockServer;
    
    @BeforeEach
    void setUp() {
        mockServer = MockRestServiceServer.createServer(restTemplate);
    }
    
    @Test
    void testGetUserById_IntegrationTest() throws Exception {
        // Given
        String expectedUrl = "https://jsonplaceholder.typicode.com/users/1";
        String responseJson = "{\"id\":1,\"name\":\"John Doe\",\"email\":\"john@example.com\"}";
        
        mockServer.expect(requestTo(expectedUrl))
                .andExpect(method(HttpMethod.GET))
                .andRespond(withSuccess(responseJson, MediaType.APPLICATION_JSON));
        
        // When
        User user = apiService.getUserById(1L);
        
        // Then
        assertThat(user.getName()).isEqualTo("John Doe");
        mockServer.verify();
    }
}

Common Pitfalls and Best Practices

Avoid these frequent mistakes when working with RestTemplate:

  • Resource Leaks: Always configure proper connection pooling and timeouts to prevent resource exhaustion
  • Exception Handling: Don’t rely on default error handling; implement custom ResponseErrorHandler for better control
  • Blocking Operations: Remember RestTemplate is synchronous and will block threads; consider WebClient for reactive applications
  • Hardcoded URLs: Use configuration properties and UriComponentsBuilder for dynamic URL construction
  • Missing Headers: Always set appropriate Content-Type and Accept headers
  • Security: Never log sensitive data like authentication tokens or passwords

Performance-focused configuration example:

@ConfigurationProperties(prefix = "app.rest-client")
@Data
public class RestClientProperties {
    private int connectTimeout = 5000;
    private int readTimeout = 30000;
    private int maxConnections = 100;
    private int maxConnectionsPerRoute = 20;
    private boolean compressionEnabled = true;
}

@Configuration
@EnableConfigurationProperties(RestClientProperties.class)
public class OptimizedRestTemplateConfig {
    
    @Bean
    @ConditionalOnProperty(name = "app.rest-client.enabled", havingValue = "true", matchIfMissing = true)
    public RestTemplate optimizedRestTemplate(RestClientProperties properties) {
        
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(properties.getMaxConnections());
        connectionManager.setDefaultMaxPerRoute(properties.getMaxConnectionsPerRoute());
        
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(properties.getConnectTimeout())
                .setConnectTimeout(properties.getConnectTimeout())
                .setSocketTimeout(properties.getReadTimeout())
                .build();
        
        HttpClientBuilder clientBuilder = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig);
        
        if (properties.isCompressionEnabled()) {
            clientBuilder.addInterceptorFirst(new RequestAcceptEncoding())
                        .addInterceptorFirst(new ResponseContentEncoding());
        }
        
        CloseableHttpClient httpClient = clientBuilder.build();
        
        HttpComponentsClientHttpRequestFactory factory = 
            new HttpComponentsClientHttpRequestFactory(httpClient);
        
        return new RestTemplate(factory);
    }
}

When deploying RestTemplate-based applications on production servers, ensure your hosting environment can handle the connection load. For high-traffic applications, consider using VPS hosting for better resource control or dedicated servers for maximum performance and isolation.

For additional reference, consult the official Spring RestTemplate documentation and consider migrating to WebClient for new projects to take advantage of reactive programming benefits.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

Your email address will not be published. Required fields are marked