A production-ready REST API that scrapes liteapks.com using Playwright + a stealth plugin (to work around Cloudflare/anti-bot protection), and exposes clean, standardized JSON endpoints for consumption by any client — including a Laravel backend.
Built with a single, reused browser instance, response caching, rate limiting, and a modular architecture designed so additional scraper sources (APKPure, APKMody, APKDone, HappyMod, etc.) can be added later without touching existing code.
- Features
- Folder Structure
- Environment Variables
- Authentication
- Installation
- Development
- Production
- Docker Deployment
- Render Deployment
- API Reference
- Curl Examples
- Laravel Integration
- Architecture Notes
- Extending With New Scraper Sources
- ✅ Search LiteAPKS by keyword
- ✅ Full app detail scraping (description, screenshots, mod info, download link)
- ✅ Cloudflare bypass via
playwright-extra+puppeteer-extra-plugin-stealth - ✅ Single reused browser instance — no per-request browser launches
- ✅ Automatic browser relaunch on crash/disconnect
- ✅ In-memory caching (NodeCache) with separate TTLs for search vs. details
- ✅ Standardized JSON envelope on every response, success or failure
- ✅ API key authentication on all
/api/v1/*routes - ✅ Retry-once logic on navigation/scrape failures
- ✅ Helmet, CORS, Compression, hidden
X-Powered-By - ✅ Rate limiting (100 requests / 15 minutes per IP)
- ✅ Morgan HTTP access logs + structured console logger
- ✅ Docker-ready (Microsoft Playwright base image)
- ✅ Render-ready (
render.yamlincluded) - ✅ ES Modules throughout, modern async/await, JSDoc comments
liteapks-api/
├── src/
│ ├── app.js # Express app assembly (middleware, routes)
│ ├── server.js # Entrypoint: boots browser then HTTP server
│ ├── browser/
│ │ └── browser.js # BrowserManager singleton (launch/reuse/relaunch)
│ ├── routes/
│ │ ├── health.js # GET /health
│ │ └── liteapks.js # GET /api/v1/liteapks/*
│ ├── controllers/
│ │ └── liteapks.controller.js # HTTP request/response shaping
│ ├── services/
│ │ ├── liteapks.service.js # Business logic: cache-aware orchestration
│ │ └── cache.service.js # NodeCache wrapper
│ ├── scraper/
│ │ └── liteapks.scraper.js # Raw Playwright scraping logic + retry
│ ├── middleware/
│ │ ├── errors.js # 404 handler + centralized error handler
│ │ └── rateLimiter.js # express-rate-limit configuration
│ └── utils/
│ └── logger.js # Timestamped console logger
├── package.json
├── Dockerfile
├── render.yaml
├── .env.example
└── .gitignore
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Port the HTTP server listens on |
NODE_ENV |
development |
development or production |
CORS_ORIGIN |
* |
Allowed CORS origin (single origin or *) |
API_KEY |
(required) | Secret API key required to access protected routes |
Copy .env.example to .env and adjust as needed:
cp .env.example .envExample .env:
PORT=3000
NODE_ENV=development
CORS_ORIGIN=*
API_KEY=your-super-secret-api-keyAll API endpoints under:
/api/v1/liteapks/*
are protected using an API Key. The public / and /health endpoints do
not require authentication.
Every protected request must include the following HTTP header:
x-api-key: YOUR_API_KEYThe API key is configured through the API_KEY environment variable:
API_KEY=your-super-secret-api-key| Scenario | HTTP Status | Message |
|---|---|---|
Missing x-api-key header |
401 | API key is required. |
| Invalid/incorrect API key | 403 | Invalid API key. |
- Never commit your real
API_KEYto version control — keep it only in.env(which is git-ignored) or in your host's environment/secret manager. - Rotate the key if it's ever exposed (e.g. committed by mistake, leaked in logs, or shared in a support channel).
- On Render, set
API_KEYas an environment variable in the service's Environment tab (or viarender.yamlwithsync: false) rather than hardcoding it anywhere in the repo.
Requires Node.js 22+.
git clone <your-repo-url> liteapks-api
cd liteapks-api
npm installnpm install runs a postinstall script (playwright install --with-deps chromium)
that downloads the Chromium binary Playwright needs. This can take a minute the
first time.
npm run devThis uses Node's built-in --watch flag to restart the server on file changes.
The server starts on http://localhost:3000 (or PORT from your .env).
npm install
npm startFor production deployments, set NODE_ENV=production — this switches Morgan
to the combined log format and suppresses debug-level logs and stack traces
in error responses.
The included Dockerfile is based on Microsoft's official Playwright image
(mcr.microsoft.com/playwright), which already bundles every OS-level
dependency Chromium needs (fonts, codecs, sandboxing libraries) — this avoids
the common "missing shared library" failures you get with a plain node
base image.
Build and run:
docker build -t liteapks-api .
docker run -p 3000:3000 --env-file .env liteapks-apiThe container runs as the non-root pwuser user (provided by the base image)
and exposes a Docker HEALTHCHECK that hits /health every 30 seconds.
This repo includes a render.yaml for one-click Render
deployment using the "Blueprint" flow:
- Push this repository to GitHub.
- In Render, choose New > Blueprint and point it at your repo.
- Render reads
render.yaml, builds the Docker image, and deploys automatically. - Render's health checks hit
GET /health, which reports whether the underlying browser is currently connected.
render.yaml sets PORT, NODE_ENV=production, and CORS_ORIGIN=* as
environment variables — adjust CORS_ORIGIN to your Laravel app's domain in
production for tighter security.
You must also configure API_KEY as an environment variable on the Render
service (Dashboard → your service → Environment → Add Environment
Variable, or via render.yaml using sync: false so the value isn't stored
in the repo). Without it set, protected /api/v1/liteapks/* routes will
reject all requests.
If you'd rather deploy without Docker (Render's native Node runtime), set:
- Build Command:
npm install - Start Command:
npm start
Render's native Node environment will run the postinstall script
automatically, downloading Chromium during the build step.
All responses use this envelope:
{
"success": true,
"cached": false,
"data": { }
}Errors always look like:
{
"success": false,
"message": "Human readable explanation"
}Authentication: Endpoints under
/api/v1/liteapks/*require thex-api-keyheader (see Authentication).GET /andGET /healthare public and do not require an API key.
Basic service identity check.
{
"success": true,
"name": "LiteAPKS API",
"version": "1.0.0"
}Reports process + browser health. Used by Docker/Render health checks.
{
"success": true,
"browser": "connected",
"uptime": 1234,
"memory": "128 MB"
}Searches LiteAPKS for a keyword.
Query params
| Param | Required | Description |
|---|---|---|
q |
yes | Search keyword |
Response
{
"success": true,
"cached": false,
"count": 10,
"data": [
{
"id": "whatsapp-messenger",
"title": "WhatsApp Messenger MOD APK",
"clean_title": "WhatsApp Messenger",
"url": "https://liteapks.com/whatsapp-messenger.html",
"image": "https://liteapks.com/wp-content/uploads/whatsapp.png",
"version": "2.24.15.75",
"size": "45 MB",
"mod_info": "Unlocked Premium Features",
"author": "LiteAPKS",
"is_mod": true
}
]
}Cached for 10 minutes per keyword.
Fetches full details for a single app.
Response
{
"success": true,
"cached": false,
"data": {
"title": "WhatsApp Messenger v2.24.15.75 MOD APK",
"clean_title": "WhatsApp Messenger",
"version": "2.24.15.75",
"size": "45 MB",
"developer": "WhatsApp LLC",
"package_name": "com.whatsapp",
"category": "Communication",
"image": "https://liteapks.com/wp-content/uploads/whatsapp.png",
"screenshots": [
"https://liteapks.com/wp-content/uploads/ss1.png",
"https://liteapks.com/wp-content/uploads/ss2.png"
],
"description": "WhatsApp Messenger is a free messaging app...",
"description_html": "<p>WhatsApp Messenger is a free messaging app...</p>",
"mod_info": ["Unlocked Premium Features", "No Ads"],
"mod_info_text": "Unlocked Premium Features, No Ads",
"download_url": "https://liteapks.com/download/whatsapp-messenger"
}
}Cached for 30 minutes per slug.
| Scenario | HTTP Status | Message |
|---|---|---|
Missing x-api-key header |
401 | API key is required. |
| Invalid/incorrect API key | 403 | Invalid API key. |
Missing q param |
400 | Query parameter 'q' is required |
| Invalid slug format | 400 | Invalid slug format |
| App not found (404 from source) | 404 | The requested app was not found. |
| Navigation timeout | 504 | The request to LiteAPKS timed out. Please try again. |
| Cloudflare / anti-bot block | 503 | LiteAPKS is currently blocking automated access... |
| Browser/network failure | 502 | Upstream browser/network error. Please retry. |
| Rate limit exceeded | 429 | Too many requests. Please try again later. |
| Unexpected error | 500 | Internal server error. |
# Root (public — no API key needed)
curl https://your-app.onrender.com/
# Health check (public — no API key needed)
curl https://your-app.onrender.com/health
# Search (requires API key)
curl -H "x-api-key: YOUR_API_KEY" \
"https://your-app.onrender.com/api/v1/liteapks/search?q=whatsapp"
# Details (requires API key)
curl -H "x-api-key: YOUR_API_KEY" \
"https://your-app.onrender.com/api/v1/liteapks/details/whatsapp-messenger"Since this API returns a standardized JSON envelope, integrating it into a Laravel app (e.g. the admin panel app-directory scraper mentioned in your project) is straightforward using Laravel's HTTP client.
<?php
// app/Services/LiteApksClient.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class LiteApksClient
{
protected string $baseUrl;
protected string $apiKey;
public function __construct()
{
$this->baseUrl = config('services.liteapks.base_url', 'https://your-app.onrender.com');
$this->apiKey = config('services.liteapks.api_key');
}
public function search(string $keyword): array
{
$response = Http::timeout(60)
->withHeaders(['x-api-key' => $this->apiKey])
->get("{$this->baseUrl}/api/v1/liteapks/search", ['q' => $keyword]);
$response->throw();
return $response->json('data', []);
}
public function details(string $slug): array
{
$response = Http::timeout(60)
->withHeaders(['x-api-key' => $this->apiKey])
->get("{$this->baseUrl}/api/v1/liteapks/details/{$slug}");
$response->throw();
return $response->json('data', []);
}
}Register the base URL and API key in config/services.php:
'liteapks' => [
'base_url' => env('LITEAPKS_API_URL', 'https://your-app.onrender.com'),
'api_key' => env('LITEAPKS_API_KEY'),
],And in .env:
LITEAPKS_API_URL=https://your-app.onrender.com
LITEAPKS_API_KEY=your-super-secret-api-key
Usage in a controller or job:
$client = new \App\Services\LiteApksClient();
$results = $client->search('whatsapp');
$details = $client->details($results[0]['id']);Because every response follows the { success, cached, data } envelope,
your Laravel Http::throw() calls will raise on non-2xx statuses, and
success: false bodies (400/401/403/404/503/etc.) carry a human-readable
message you can log or surface to an admin UI.
src/browser/browser.js exports a BrowserManager singleton that:
- Launches Chromium once, when the server boots (
server.jsawaitsbrowserManager.launch()before callingapp.listen()). - Keeps one
BrowserContextalive for the life of the process. - Opens a new page per request (
getPage()), never a new browser. - Listens for the browser's
disconnectedevent; if the browser crashes, the nextgetPage()call transparently relaunches it.
src/services/cache.service.js wraps NodeCache behind a small interface
(get, set, del, flush, stats) so it can be swapped for Redis later
without touching any controller or service code. TTLs are enforced at the
call site in liteapks.service.js:
- Search results: 10 minutes (
search:<keyword>) - App details: 30 minutes (
details:<slug>)
src/scraper/liteapks.scraper.js's withRetry() helper wraps every scrape:
on failure, it closes the failed page, opens a fresh one, and retries once
before propagating the error up to the centralized error handler.
src/middleware/errors.js classifies thrown errors (timeout, Cloudflare
block, not-found, browser/network failure, or generic) into the right HTTP
status code and a friendly message — the process never crashes on a scraping
failure.
The architecture is intentionally source-agnostic. To add a new source (e.g. APKPure), you would add — without modifying any existing file:
src/scraper/apkpure.scraper.js # raw Playwright scraping logic
src/services/apkpure.service.js # cache-aware orchestration
src/controllers/apkpure.controller.js
src/routes/apkpure.js
Then mount the new route in app.js:
import apkpureRoutes from "./routes/apkpure.js";
app.use("/api/v1/apkpure", apkpureRoutes);Because BrowserManager is a shared singleton, every new scraper source
reuses the same browser instance — no additional Chromium processes are
spawned per source.