Why Spring Boot Applications Use a Screenshot API
Spring Boot microservices that need screenshot or PDF generation face the same headless browser challenge as any other backend: adding Playwright or Selenium creates a heavyweight dependency that conflicts with the clean microservice architecture Spring applications typically use. A Spring Boot service is usually a compact 50–100 MB JAR that starts in under 10 seconds and deploys to Kubernetes with tight resource limits. Adding a headless Chrome dependency inflates the Docker image by 400+ MB, increases startup time by 5–10 seconds while waiting for the browser process, and requires 400–600 MB of additional RAM allocation in the pod spec. SnapAPI integrates as a standard REST call — Spring's RestTemplate or WebClient handles it identically to any other external API call — keeping the microservice small, fast, and resource-efficient.
RestTemplate Screenshot Service
@Service
public class ScreenshotService {
@Value("${snapapi.key}")
private String apiKey;
private final RestTemplate restTemplate;
public ScreenshotService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public byte[] screenshot(String url, String format, boolean fullPage) {
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl("https://snapapi.pics/screenshot")
.queryParam("access_key", apiKey)
.queryParam("url", url)
.queryParam("format", format)
.queryParam("full_page", fullPage ? "1" : "0")
.queryParam("viewport_width", "1280")
.queryParam("viewport_height", "800");
ResponseEntity response = restTemplate.exchange(
builder.toUriString(),
HttpMethod.GET,
null,
byte[].class
);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new RuntimeException("SnapAPI error: " + response.getStatusCode());
}
return response.getBody();
}
public byte[] pdf(String url) {
return screenshot(url, "pdf", true);
}
}
// Configuration bean
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofSeconds(10))
.setReadTimeout(Duration.ofSeconds(60))
.build();
}
}
WebClient Reactive Integration
@Service
public class ReactiveScreenshotService {
@Value("${snapapi.key}")
private String apiKey;
private final WebClient webClient;
public ReactiveScreenshotService(WebClient.Builder builder) {
this.webClient = builder
.baseUrl("https://snapapi.pics")
.build();
}
public Mono screenshotAsync(String url, String format) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/screenshot")
.queryParam("access_key", apiKey)
.queryParam("url", url)
.queryParam("format", format)
.queryParam("full_page", "1")
.build())
.retrieve()
.onStatus(status -> !status.is2xxSuccessful(),
res -> Mono.error(new RuntimeException("SnapAPI error: " + res.statusCode())))
.bodyToMono(byte[].class);
}
public Flux batchScreenshots(List urls) {
return Flux.fromIterable(urls)
.flatMap(url -> screenshotAsync(url, "png"), 5); // max 5 concurrent
}
}
Async Screenshot with @Async and CompletableFuture
@Service
@EnableAsync
public class AsyncScreenshotService {
private final ScreenshotService screenshotService;
private final PageRepository pageRepository;
@Async("screenshotExecutor")
public CompletableFuture captureAndSave(Long pageId, String url) {
try {
byte[] png = screenshotService.screenshot(url, "png", true);
pageRepository.updateScreenshot(pageId, png, Instant.now());
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
}
// Thread pool configuration
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(5);
exec.setMaxPoolSize(20);
exec.setQueueCapacity(200);
exec.setThreadNamePrefix("screenshot-");
exec.initialize();
return exec;
}
}
Spring Boot REST Controller — PDF Download
@RestController
@RequestMapping("/api/v1")
public class ScreenshotController {
private final ScreenshotService screenshotService;
@GetMapping("/screenshot")
public ResponseEntity screenshot(
@RequestParam String url,
@RequestParam(defaultValue = "png") String format) {
byte[] data = screenshotService.screenshot(url, format, true);
String contentType = "pdf".equals(format) ? "application/pdf" : "image/png";
String filename = "pdf".equals(format) ? "page.pdf" : "screenshot.png";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, contentType)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename="" + filename + """)
.header(HttpHeaders.CACHE_CONTROL, "public, max-age=3600")
.body(data);
}
}
Scheduled Screenshot Job with @Scheduled
Spring's @Scheduled annotation makes it straightforward to run periodic screenshot capture jobs without a separate job scheduler. Combine it with the async screenshot service to capture screenshots of monitored URLs on a cron schedule and store results to an S3 bucket or database:
@Component
public class ScreenshotMonitorJob {
private final ScreenshotService screenshotService;
private final MonitoredPageRepository monitorRepo;
private final StorageService storage;
@Scheduled(cron = "0 0 */6 * * *") // every 6 hours
public void captureMonitoredPages() {
List pages = monitorRepo.findAllActive();
pages.parallelStream().forEach(page -> {
try {
byte[] png = screenshotService.screenshot(page.getUrl(), "png", true);
String key = storage.save(page.getId(), png);
monitorRepo.updateLastCapture(page.getId(), key, Instant.now());
} catch (Exception e) {
log.error("Screenshot failed for {}: {}", page.getUrl(), e.getMessage());
}
});
}
}
Environment Configuration for Spring Boot
Store the SnapAPI key in Spring's externalized configuration. Add snapapi.key=${SNAPAPI_KEY} to application.properties and set the SNAPAPI_KEY environment variable in your deployment environment. In Kubernetes, create a Secret for the API key and reference it as an environment variable in the pod spec rather than embedding the key in a ConfigMap. For local development, set the environment variable in your IDE run configuration or in a .env file loaded by a dotenv plugin. Never commit the API key to version control — use Spring's encrypted property support or HashiCorp Vault integration for production secret management in environments that require secrets rotation or audit trails.