A production-ready scraping API that crawls a public website and normalizes its content into deterministic JSON. Built with Spring Boot 3, Java 21, jsoup, Caffeine, and the official Bright Data Web Scraper API v3.
Integrating the API? See DOCUMENTATION-AND-GUIDE.md for the full API contract, status/error codes, rate limits, frontend and middleware guides. Live instance:
https://scrapeverse-api.onrender.com(Swagger UI at/swagger-ui.html).Terminal client: an interactive Textual dashboard + headless CLI lives in
tui-client/(pip install -r tui-client/requirements.txt, thenpython tui-client/scrapeverse_tui.py).
- Crawl engine: deterministic sequential BFS with
maxPages/maxDepthcaps, same-domain crawling, URL normalization/deduplication, canonical URL detection and rel=next / URL-pattern pagination. - Content extraction: semantic HTML extraction (headings, text, links, images, tables, lists, forms, content blocks) plus meta / Open Graph / Twitter Card / JSON-LD structured data.
- Providers:
brightdata(datasets v3 API — Web Scraper / Crawl API / Live Crawler; trigger → progress → snapshot, retries, circuit breaker),brightdata-unlocker(Web Unlocker API — real-browser JS rendering for SPAs) ordirect(plain compliant GET for static sites and local dev).SCRAPER_PROVIDER=autopicks Bright Data when configured, otherwise the Web Unlocker, otherwise direct. Failover chains providers in the same order. - Security:
- SSRF protection on the start URL and every discovered URL: scheme allow-list, blocked hostname namespaces, DNS resolution + double-resolution (DNS rebinding) checks, and IPv4/IPv6/CIDR block lists.
- robots.txt compliance (standard
*rules) with a short-lived cache. - In-memory fixed-window rate limiting per client IP (
429+Retry-After). - Request-body size filter, security headers, per-request MDC request IDs, and URL query sanitization in logs. Secrets are never logged.
- API contract: stable
success/request/site/pages/statistics/warnings/errorsshape with every key always present. Errors use a consistent{"success":false,"error":{"code","message","details"}}shape with no stack traces.
- JDK 21
- Maven 3.9+
- Docker (only for
ContainerizedScrapeIT, skipped automatically when absent)
mvn clean package # build the jar
mvn test # unit + integration tests (164 tests)
mvn verify # also runs failsafe ITs (incl. Docker-based test)
java -jar target/scrapeverse-api.jarexport SCRAPER_PROVIDER=direct # no Bright Data credentials needed
mvn spring-boot:run
# or
java -jar target/scrapeverse-api.jarThen:
curl http://localhost:8080/api/v1/health
curl http://localhost:8080/api/v1/info
curl -X POST http://localhost:8080/api/v1/scrape \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'
# GET variant (url must be URL-encoded)
curl -G http://localhost:8080/api/v1/scrape \
--data-urlencode "url=https://example.com" \
--data-urlencode "maxPages=10" \
--data-urlencode "includeImages=false"Interactive OpenAPI docs: http://localhost:8080/swagger-ui.html
Everything is configurable through environment variables (defaults in
src/main/resources/application.yml). See .env.example for the full list.
| Variable | Default | Purpose |
|---|---|---|
PORT |
8080 |
HTTP port |
SCRAPER_PROVIDER |
auto |
auto / brightdata / brightdata-unlocker / direct |
SCRAPER_MAX_PAGES |
25 |
max pages per scrape |
SCRAPER_MAX_DEPTH |
2 |
max crawl depth |
SCRAPER_TIMEOUT_MS |
60000 |
hard deadline per scrape |
SCRAPER_RESPECT_ROBOTS |
true |
obey robots.txt |
SCRAPER_SAME_DOMAIN_ONLY |
true |
stay on the starting domain |
SCRAPER_RATE_LIMIT |
30 |
requests per window per client IP |
SCRAPER_RATE_LIMIT_WINDOW_MS |
60000 |
rate limit window |
SCRAPER_CACHE_ENABLED |
true |
in-memory page cache |
BRIGHTDATA_API_KEY |
empty | Bright Data API token |
BRIGHTDATA_DATASET_ID |
empty | Bright Data dataset id |
BRIGHTDATA_FORMAT |
json |
output format: json/html/markdown/text |
BRIGHTDATA_API_BASE_URL |
https://api.brightdata.com |
API base URL |
BRIGHTDATA_UNLOCKER_ZONE |
empty | Web Unlocker zone name (JS rendering) |
BRIGHTDATA_UNLOCKER_API_KEY |
empty | Web Unlocker token (defaults to BRIGHTDATA_API_KEY) |
BRIGHTDATA_UNLOCKER_FORMAT |
raw |
Web Unlocker output format: raw/markdown |
BRIGHTDATA_UNLOCKER_RENDER |
true |
force browser JavaScript rendering |
CORS_ALLOWED_ORIGINS |
* |
allowed CORS origins |
Same endpoint is also available via GET /api/v1/scrape with the request fields
as query parameters (url required and URL-encoded; maxPages, maxDepth,
includeLinks, includeImages, includeMetadata, includeTables,
includeStructuredData optional).
Request body:
{
"url": "https://example.com",
"maxPages": 10,
"maxDepth": 2,
"includeLinks": true,
"includeImages": true,
"includeMetadata": true,
"includeTables": true,
"includeStructuredData": true
}maxPages and maxDepth are capped by server configuration. The include*
flags empty the corresponding response fields when false.
Example response (abbreviated):
{
"success": true,
"request": {
"url": "https://example.com",
"timestamp": "2026-08-15T05:36:42Z",
"pagesRequested": 25,
"pagesProcessed": 1
},
"site": { "title": "Example Domain", "description": null, "language": "en",
"canonicalUrl": null, "robots": null, "favicon": null },
"pages": [
{
"url": "https://example.com",
"depth": 0,
"title": "Example Domain",
"headings": { "h1": ["Example Domain"], "h2": [], "h3": [], "h4": [], "h5": [], "h6": [] },
"links": [{ "text": "Learn more", "url": "https://iana.org/domains/example", "rel": [] }],
"images": [], "tables": [], "lists": [], "forms": [],
"metadata": {}, "structuredData": [], "openGraph": {}, "twitterCard": {},
"schemaOrg": [], "contentBlocks": []
}
],
"statistics": { "pagesDiscovered": 1, "pagesProcessed": 1, "linksFound": 1,
"imagesFound": 0, "tablesFound": 0, "formsFound": 0 },
"warnings": [],
"errors": []
}Error codes: INVALID_URL, UNSUPPORTED_PROTOCOL, SSRF_BLOCKED,
ROBOTS_BLOCKED, INVALID_REQUEST, PAYLOAD_TOO_LARGE, RATE_LIMITED,
PROVIDER_ERROR, PROVIDER_TIMEOUT, PROVIDER_UNAVAILABLE,
PROVIDER_AUTH_ERROR, INVALID_PROVIDER_RESPONSE, EXTRACTION_FAILED,
SCRAPE_TIMEOUT, CRAWL_LIMIT_REACHED, INTERNAL_ERROR.
Example error:
{
"success": false,
"error": { "code": "SSRF_BLOCKED", "message": "hostname '127.0.0.1' is a blocked address", "details": {} }
}{ "status": "UP", "service": "ScrapeVerse API", "version": "1.0.0",
"provider": { "name": "direct", "configured": true, "supportsRendering": false,
"circuitState": "CLOSED", "consecutiveFailures": 0 } }Provider capabilities and configured limits.
docker build -t scrapeverse-api .
docker run -p 8080:8080 -e SCRAPER_PROVIDER=direct scrapeverse-api- Push the repository to GitHub.
- In Render, choose New → Blueprint, select the repo and the
render.yamlblueprint, or create a new Web Service pointing at the repo (build commandmvn clean package -DskipTests, start commandjava -jar target/scrapeverse-api.jar, health check path/api/v1/health). - Set environment variables in the Render dashboard (see
.env.example). For production setBRIGHTDATA_API_KEY,BRIGHTDATA_DATASET_IDandSCRAPER_PROVIDER=brightdata; for JS-heavy targets also setBRIGHTDATA_UNLOCKER_ZONEand useSCRAPER_PROVIDER=brightdata-unlocker.
The brightdata provider targets the datasets v3 API shared by the Web
Scraper, Crawl API and Live Crawler products. To scrape arbitrary
websites (JS-rendered, bot-protected):
- In the Bright Data Control Panel open the Crawl API page and create a dataset (no-code: enter a root URL; or via API).
- Copy the Crawl API dataset id (format
gd_xxxxxxxx) intoBRIGHTDATA_DATASET_ID, and the product API token intoBRIGHTDATA_API_KEY. - Optionally set
BRIGHTDATA_FORMAT=json(default, structured),html(raw HTML),markdown, ortext. Markdown/Text records are wrapped into HTML so the extraction layer can still produce titles, text and metadata. - Set
SCRAPER_PROVIDER=brightdata(orauto).
JS-rendered sites (SPAs): client-side rendered apps (React/Vue, <div id="root">)
serve an empty shell over the wire. Two ways to get the rendered DOM:
- Preferred — Web Unlocker (
brightdata-unlockerprovider): create a zone in the Control Panel (Web Access → Web Unlocker → Create zone), setBRIGHTDATA_UNLOCKER_ZONEand eitherBRIGHTDATA_UNLOCKER_API_KEYor the sharedBRIGHTDATA_API_KEY, then setSCRAPER_PROVIDER=brightdata-unlocker(orauto). The provider callsPOST /requestwithrender:trueand returns the fully rendered page. - Crawl API dataset: the dataset must have JavaScript rendering / unlocking enabled in the Control Panel to return the rendered DOM (not all datasets expose this toggle).
Until rendering is available:
- A page that returned only the un-rendered shell is reported with a
JS_RENDER_REQUIREDwarning. - Client-side routes the server does not serve (origin 404, e.g.
/projectson a history-router SPA) are reported as HTTP404 NOT_FOUNDinstead of a generic extraction failure. The trigger always requestsinclude_errors=trueso Bright Data includes per-record error detail.
Do not use a site-specific Web Scraper dataset (e.g. an Amazon scraper) unless you only target that site — those reject URLs for other domains.
- SSRF checks run on the initial URL and on every URL discovered during the crawl, before any outbound request.
- robots.txt is honored per origin and cached briefly.
- Responses never include stack traces or internal class names.
- Log lines carry a
requestId/correlationId; sensitive URL query parameters and API credentials are redacted.
The service recovers from provider outages without manual intervention:
- Per-provider circuit breakers trip OPEN after N consecutive failures
(
SCRAPER_CIRCUIT_BREAKER_FAILURE_THRESHOLD) and re-enter service with a probe request afterSCRAPER_CIRCUIT_BREAKER_OPEN_DURATION_MS. Each provider has its own breaker, so one provider's outage never blocks the other. - Transient retry retries flaky provider calls (timeouts, 5xx, rate
limits) with exponential backoff before counting a failure
(
SCRAPER_RETRY_MAX_ATTEMPTS,SCRAPER_RETRY_BACKOFF_*). - Failover routes page fetches to the other configured provider when the
primary is unavailable (
SCRAPER_FAILOVER_ENABLED, default on). - Active recovery probe periodically issues a lightweight fetch through a
degraded provider so recovery is detected even with zero user traffic
(
SCRAPER_PROBE_ENABLED,SCRAPER_PROBE_INTERVAL_MS,SCRAPER_PROBE_URL). /api/v1/healthreports each provider's circuit state underprovider.circuitsand returnsDEGRADEDonly when every provider circuit is open. Docker HEALTHCHECK and the Render health check restart the container when the process itself is unhealthy.
- The in-memory rate limiter and caches are single-instance; scale horizontally with a shared store if you run many replicas.
- The
directprovider cannot execute JavaScript and will report structured failures for sites that block standard HTTP clients. Usebrightdatafor rendered/JS-heavy targets.
- Unit: URL/SSRF validation, robots parsing, normalization, rate limiting, circuit breaker, HTML content parsing, all providers (MockWebServer), crawl orchestration.
- Integration: full Spring context via MockMvc (contract, validation, SSRF, rate limit, provider failure mapping), and an end-to-end crawl against a local MockWebServer site graph.
ContainerizedScrapeITruns the full pipeline against a real nginx container when Docker is available and is skipped otherwise.