JUHE API Marketplace
Comprehensive Documentation

API Documentation

Everything you need to integrate and use our APIs effectively with guides, references, and examples

Java

9 min read

This guide provides a comprehensive Java implementation for integrating with JUHE API using Spring Boot, featuring enterprise-grade error handling, resilience patterns, and best practices.

Prerequisites

  • Java 11 or higher
  • Maven or Gradle build tool
  • Basic understanding of Spring Boot
  • IDE such as IntelliJ IDEA or Eclipse

Project Setup

Maven Dependencies

Add the following dependencies to your pom.xml:

<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- HTTP Client -->
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.13</version>
    </dependency>

    <!-- Jackson for JSON processing -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>

    <!-- Resilience4j for circuit breaking -->
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-spring-boot2</artifactId>
        <version>1.7.1</version>
    </dependency>

    <!-- Caffeine Cache -->
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>

    <!-- Lombok to reduce boilerplate -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

Gradle Dependencies

Or if you're using Gradle, add to your build.gradle:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.apache.httpcomponents:httpclient:4.5.13'
    implementation 'com.fasterxml.jackson.core:jackson-databind'
    implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.1'
    implementation 'com.github.ben-manes.caffeine:caffeine'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

API Client Implementation

Model Classes

First, let's create the model classes for the responses:

// Location.java
package com.example.juhe.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

@Data
public class Location {
    private String name;
    private String country;
    @JsonProperty("lat")
    private double latitude;
    @JsonProperty("lon")
    private double longitude;
}
// WeatherCondition.java
package com.example.juhe.model;

import lombok.Data;

@Data
public class WeatherCondition {
    private String text;
    private String icon;
}
// CurrentWeather.java
package com.example.juhe.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

@Data
public class CurrentWeather {
    @JsonProperty("temp_c")
    private double temperatureCelsius;
    @JsonProperty("temp_f")
    private double temperatureFahrenheit;
    private WeatherCondition condition;
    private int humidity;
    @JsonProperty("wind_kph")
    private double windSpeedKph;
    @JsonProperty("wind_mph")
    private double windSpeedMph;
}
// WeatherResponse.java
package com.example.juhe.model;

import lombok.Data;

@Data
public class WeatherResponse {
    private String status;
    private WeatherData data;
}

@Data
class WeatherData {
    private Location location;
    private CurrentWeather current;
}
// ForecastDay.java
package com.example.juhe.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

@Data
public class ForecastDay {
    private String date;
    @JsonProperty("max_temp_c")
    private double maxTemperatureCelsius;
    @JsonProperty("min_temp_c")
    private double minTemperatureCelsius;
    private String condition;
    @JsonProperty("chance_of_rain")
    private int chanceOfRain;
}
// ForecastResponse.java
package com.example.juhe.model;

import lombok.Data;

import java.util.List;

@Data
public class ForecastResponse {
    private String status;
    private ForecastData data;
}

@Data
class ForecastData {
    private Location location;
    private List<ForecastDay> forecast;
}

API Exception Handling

Create a custom exception for API errors:

// JuheApiException.java
package com.example.juhe.exception;

import lombok.Getter;

@Getter
public class JuheApiException extends RuntimeException {
    private final String errorCode;
    private final int statusCode;

    public JuheApiException(String message, String errorCode, int statusCode) {
        super(message);
        this.errorCode = errorCode;
        this.statusCode = statusCode;
    }

    public JuheApiException(String message, String errorCode, int statusCode, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
        this.statusCode = statusCode;
    }
}

API Client

Now, let's implement the robust API client:

// JuheApiClient.java
package com.example.juhe.client;

import com.example.juhe.exception.JuheApiException;
import com.example.juhe.model.ForecastResponse;
import com.example.juhe.model.WeatherResponse;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;

@Component
@Slf4j
public class JuheApiClient {

    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper;
    private final String apiKey;
    private final String baseUrl;

    public JuheApiClient(
            ObjectMapper objectMapper,
            @Value("${juhe.api.key}") String apiKey,
            @Value("${juhe.api.base-url:<https://hub.juheapi.com>}") String baseUrl) {
        this.objectMapper = objectMapper;
        this.apiKey = apiKey;
        this.baseUrl = baseUrl;
        this.restTemplate = createRestTemplate();
    }

    private RestTemplate createRestTemplate() {
        // Configure connection pooling
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(100);
        connectionManager.setDefaultMaxPerRoute(20);

        // Configure timeouts
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(5000)
                .setSocketTimeout(10000)
                .build();

        // Build HttpClient
        CloseableHttpClient httpClient = HttpClientBuilder.create()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig)
                .build();

        // Create request factory with the HttpClient
        HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);

        return new RestTemplate(requestFactory);
    }

    /**
     * Make a GET request to the JUHE API.
     *
     * @param endpoint API endpoint
     * @param params Query parameters
     * @param responseType Response class
     * @param <T> Response type
     * @return API response
     */
    @CircuitBreaker(name = "juheApi")
    @Retry(name = "juheApi")
    public <T> T get(String endpoint, Map<String, Object> params, Class<T> responseType) {
        try {
            // Add API key to parameters
            UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseUrl + endpoint);

            // Add query parameters
            if (params != null) {
                params.forEach(builder::queryParam);
            }

            // Add API key
            builder.queryParam("key", apiKey);

            // Set up headers
            HttpHeaders headers = new HttpHeaders();
            headers.set("Authorization", "Bearer " + apiKey);

            // Make the request
            ResponseEntity<String> response = restTemplate.exchange(
                    builder.toUriString(),
                    HttpMethod.GET,
                    new HttpEntity<>(headers),
                    String.class
            );

            // Check for error in response
            String responseBody = response.getBody();
            JsonNode root = objectMapper.readTree(responseBody);

            if ("error".equals(root.path("status").asText())) {
                String errorCode = root.path("code").asText("UNKNOWN_ERROR");
                String errorMessage = root.path("message").asText("Unknown error occurred");
                throw new JuheApiException(errorMessage, errorCode, response.getStatusCodeValue());
            }

            // Parse successful response
            return objectMapper.readValue(responseBody, responseType);

        } catch (HttpStatusCodeException e) {
            // Handle HTTP errors
            try {
                JsonNode errorBody = objectMapper.readTree(e.getResponseBodyAsString());
                String errorCode = errorBody.path("code").asText("HTTP_ERROR");
                String errorMessage = errorBody.path("message").asText(e.getStatusText());
                throw new JuheApiException(errorMessage, errorCode, e.getRawStatusCode(), e);
            } catch (IOException ioException) {
                throw new JuheApiException(e.getStatusText(), "HTTP_ERROR", e.getRawStatusCode(), e);
            }
        } catch (IOException e) {
            // Handle JSON parsing errors
            throw new JuheApiException("Error parsing API response", "PARSE_ERROR", 500, e);
        } catch (JuheApiException e) {
            // Rethrow API exceptions
            throw e;
        } catch (Exception e) {
            // Handle other exceptions
            throw new JuheApiException("Unexpected error: " + e.getMessage(), "UNEXPECTED_ERROR", 500, e);
        }
    }

    /**
     * Get current weather for a location.
     *
     * @param location Location name
     * @return Weather response
     */
    @Cacheable(value = "weatherCache", key = "#location")
    public WeatherResponse getCurrentWeather(String location) {
        log.info("Fetching current weather for location: {}", location);
        return get("/weather/current", Collections.singletonMap("location", location), WeatherResponse.class);
    }

    /**
     * Get weather forecast for a location.
     *
     * @param location Location name
     * @param days Number of days (default: 5)
     * @return Forecast response
     */
    @Cacheable(value = "forecastCache", key = "#location + '-' + #days")
    public ForecastResponse getWeatherForecast(String location, int days) {
        log.info("Fetching {}-day forecast for location: {}", days, location);
        Map<String, Object> params = Map.of(
                "location", location,
                "days", days
        );
        return get("/weather/forecast", params, ForecastResponse.class);
    }
}

Service Layer

Let's create a service layer that uses our API client:

// WeatherService.java
package com.example.juhe.service;

import com.example.juhe.client.JuheApiClient;
import com.example.juhe.exception.JuheApiException;
import com.example.juhe.model.CurrentWeather;
import com.example.juhe.model.ForecastDay;
import com.example.juhe.model.ForecastResponse;
import com.example.juhe.model.WeatherResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class WeatherService {

    private final JuheApiClient apiClient;

    /**
     * Get current weather for a location.
     *
     * @param location Location name
     * @return Current weather data
     */
    public CurrentWeather getCurrentWeather(String location) {
        try {
            WeatherResponse response = apiClient.getCurrentWeather(location);
            return response.getData().getCurrent();
        } catch (JuheApiException e) {
            log.error("Error fetching current weather for {}: {} - {}",
                    location, e.getErrorCode(), e.getMessage());
            throw e;
        }
    }

    /**
     * Get weather forecast for a location.
     *
     * @param location Location name
     * @param days Number of forecast days
     * @return List of forecast days
     */
    public List<ForecastDay> getWeatherForecast(String location, int days) {
        try {
            ForecastResponse response = apiClient.getWeatherForecast(location, days);
            return response.getData().getForecast();
        } catch (JuheApiException e) {
            log.error("Error fetching forecast for {}: {} - {}",
                    location, e.getErrorCode(), e.getMessage());
            throw e;
        }
    }
}

Controller Layer

Now, let's create REST endpoints that use our service:

// WeatherController.java
package com.example.juhe.controller;

import com.example.juhe.exception.JuheApiException;
import com.example.juhe.model.CurrentWeather;
import com.example.juhe.model.ForecastDay;
import com.example.juhe.service.WeatherService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/weather")
@RequiredArgsConstructor
public class WeatherController {

    private final WeatherService weatherService;

    @GetMapping("/{location}")
    public ResponseEntity<Object> getCurrentWeather(@PathVariable String location) {
        try {
            CurrentWeather weather = weatherService.getCurrentWeather(location);
            return ResponseEntity.ok(Map.of(
                    "success", true,
                    "data", weather
            ));
        } catch (JuheApiException e) {
            return handleApiException(e);
        }
    }

    @GetMapping("/{location}/forecast")
    public ResponseEntity<Object> getWeatherForecast(
            @PathVariable String location,
            @RequestParam(defaultValue = "5") int days) {
        try {
            List<ForecastDay> forecast = weatherService.getWeatherForecast(location, days);
            return ResponseEntity.ok(Map.of(
                    "success", true,
                    "data", forecast
            ));
        } catch (JuheApiException e) {
            return handleApiException(e);
        }
    }

    private ResponseEntity<Object> handleApiException(JuheApiException e) {
        Map<String, Object> error = new HashMap<>();
        error.put("success", false);
        error.put("error", Map.of(
                "code", e.getErrorCode(),
                "message", e.getMessage()
        ));

        return ResponseEntity
                .status(e.getStatusCode() < 500 ? e.getStatusCode() : HttpStatus.INTERNAL_SERVER_ERROR)
                .body(error);
    }
}

Application Configuration

Let's configure the application:

// Application.java
package com.example.juhe;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class JuheApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(JuheApiApplication.class, args);
    }
}
// AppConfig.java
package com.example.juhe.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
public class AppConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .maximumSize(100));
        return cacheManager;
    }
}

Application Properties

Create an application.properties file:

# API Configuration
juhe.api.key=${JUHE_API_KEY}
juhe.api.base-url=https://hub.juheapi.com

# Resilience4j Circuit Breaker Configuration
resilience4j.circuitbreaker.instances.juheApi.failureRateThreshold=50
resilience4j.circuitbreaker.instances.juheApi.minimumNumberOfCalls=5
resilience4j.circuitbreaker.instances.juheApi.automaticTransitionFromOpenToHalfOpenEnabled=true
resilience4j.circuitbreaker.instances.juheApi.waitDurationInOpenState=10s

# Resilience4j Retry Configuration
resilience4j.retry.instances.juheApi.maxAttempts=3
resilience4j.retry.instances.juheApi.waitDuration=1s
resilience4j.retry.instances.juheApi.retryExceptions=org.springframework.web.client.ResourceAccessException

Error Handling

Let's add a global exception handler:

// GlobalExceptionHandler.java
package com.example.juhe.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.Map;

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(JuheApiException.class)
    public ResponseEntity<Object> handleApiException(JuheApiException e) {
        log.error("API Exception: {} - {}", e.getErrorCode(), e.getMessage());

        return ResponseEntity
                .status(determineHttpStatus(e))
                .body(Map.of(
                        "success", false,
                        "error", Map.of(
                                "code", e.getErrorCode(),
                                "message", e.getMessage()
                        )
                ));
    }

    private HttpStatus determineHttpStatus(JuheApiException e) {
        if (e.getStatusCode() >= 400 && e.getStatusCode() < 600) {
            return HttpStatus.valueOf(e.getStatusCode());
        }

        // Map common error codes to appropriate HTTP statuses
        return switch (e.getErrorCode()) {
            case "INVALID_API_KEY" -> HttpStatus.UNAUTHORIZED;
            case "RATE_LIMIT_EXCEEDED" -> HttpStatus.TOO_MANY_REQUESTS;
            case "INVALID_PARAMETER" -> HttpStatus.BAD_REQUEST;
            case "PARSE_ERROR" -> HttpStatus.UNPROCESSABLE_ENTITY;
            default -> HttpStatus.INTERNAL_SERVER_ERROR;
        };
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleGenericException(Exception e) {
        log.error("Unexpected error: {}", e.getMessage(), e);

        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of(
                        "success", false,
                        "error", Map.of(
                                "code", "INTERNAL_ERROR",
                                "message", "An unexpected error occurred"
                        )
                ));
    }
}

Running the Application

To run your application, set the JUHE_API_KEY environment variable and start Spring Boot:

export JUHE_API_KEY=your_api_key
./mvnw spring-boot:run

Or with Gradle:

export JUHE_API_KEY=your_api_key
./gradlew bootRun

Testing the API

Let's create a simple test to verify our client works:

// JuheApiClientTest.java
package com.example.juhe.client;

import com.example.juhe.exception.JuheApiException;
import com.example.juhe.model.WeatherResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@ActiveProfiles("test")
public class JuheApiClientTest {

    @Autowired
    private JuheApiClient apiClient;

    @Test
    public void getCurrentWeather_validLocation_returnsWeatherData() {
        // Given
        String location = "London";

        // When
        WeatherResponse response = apiClient.getCurrentWeather(location);

        // Then
        assertNotNull(response);
        assertEquals("success", response.getStatus());
        assertNotNull(response.getData());
        assertNotNull(response.getData().getLocation());
        assertNotNull(response.getData().getCurrent());
        assertEquals("London", response.getData().getLocation().getName());
    }

    @Test
    public void getCurrentWeather_invalidLocation_throwsException() {
        // Given
        String invalidLocation = "NonExistentLocation123456789";

        // When & Then
        JuheApiException exception = assertThrows(
                JuheApiException.class,
                () -> apiClient.getCurrentWeather(invalidLocation)
        );

        assertEquals("LOCATION_NOT_FOUND", exception.getErrorCode());
    }
}

Best Practices

  1. Connection Pooling: Efficient HTTP connection reuse
  2. Caching: Reduce API calls with appropriate caching
  3. Circuit Breaking: Prevent cascading failures
  4. Retry Logic: Automatically retry transient failures
  5. Error Handling: Comprehensive exception handling
  6. Logging: Detailed logging for debugging
  7. Configuration Externalization: API keys and URLs in properties
  8. Type Safety: Strongly typed DTOs for API responses
  9. Service Layer: Business logic separated from API calls
  10. Testing: Verify client behavior with unit tests

Next Steps

  • Learn about JUHE API Features for more capabilities
  • Explore Python Integration examples
  • Visit Best Practices for more optimization tips