Contributions are welcome — bug fixes, improvements, and new features.
For bug reports or feature requests, open an Issue first so the approach can be discussed before you start coding.
- Node.js 22+
- npm
- No Cloudflare account needed for local development — a local SQLite database is used
- Wrangler CLI (
npm install -g wrangler) andwrangler loginonly needed for deployment better-sqlite3(optional) — installs automatically withnpm install; only needed for the VPS/standalone server
-
Fork the repository and clone your fork:
git clone https://github.com/<your-username>/simplelikes.git cd simplelikes
-
Install dependencies and initialize config:
npm install cp .env.example .env
-
Create local D1 tables (required once):
npm run dev:setup
-
Start the local dev server (loads
.envautomatically):npm run dev
The server starts at
http://localhost:8787. You can test endpoints with curl:curl http://localhost:8787/likes/hello-world
-
Make your changes in
src/. -
Run type check and tests before opening a PR:
npm run typecheck && npm test
If you made changes to backend code (
src/), verify coverage is maintained:npm run test:coverage
The project requires ≥95% coverage (statements, branches, functions, lines). New features must include tests that cover the new code.
If you changed the client-side web component (
src/client/), also run the web component tests:npx vitest run tests/unit/client/
-
Update any documentation that became outdated with your changes — this includes inline code comments, JSDoc, README sections, or example files. If you changed the client component, rebuild the bundle:
npm run build:client
If the change introduces or modifies a user-facing feature, ensure the corresponding docs reflect it.
-
Open a Pull Request against
maindescribing what changed and why.
You do not need to bump versions or update CHANGELOG.md — versioning and releases are handled by the maintainer after the PR is merged.
The project enforces ≥95% coverage on src/ (backend). The CI pipeline breaks if coverage drops below the threshold. Always check:
npm run test:coverageThe web component in src/client/ (TypeScript source, bundled to dist/simplelikes.js) has its own test suite in tests/unit/client/ using happy-dom for DOM emulation. If you modify the component, add or update tests in that directory.
Run a full local check to make sure nothing is broken:
npm run typecheck
npm run test:coverage # backend unit tests + coverage
npx vitest run tests/unit/client/ # web component testsIntegration tests are optional for contributors (requires INTEGRATION_TEST_SECRET) and are run automatically in CI after deployment.
This project uses Conventional Commits. Every commit message must follow the format:
type: short description
| Type | When to use |
|---|---|
feat |
New feature or behavior visible to API consumers |
fix |
Bug fix |
docs |
Documentation only |
chore |
Tooling, dependencies, configuration |
ci |
CI/CD workflow changes |
refactor |
Code restructuring without behavior change |
style |
Formatting, whitespace |
test |
Tests |
perf |
Performance improvements |
The type determines how the commit appears in the CHANGELOG and influences the version bump on the next release.
The repository includes an .editorconfig file. Most editors support it natively or via plugin:
- UTF-8 encoding and LF line endings
- 2-space indentation for
.ts,.js,.json - Tab indentation for
.shscripts - Final newline and no trailing whitespace (except
.md)
See the Scripts table in README.md for the full list of available commands.
scripts/release.sh, scripts/changelog.sh, and scripts/setup.sh are maintainer-only scripts. The setup is also available via npm run setup for contributors with Cloudflare access.
npm run test:integration requires valid INTEGRATION_TEST_SECRET and EXPECTED_ORIGIN environment variables and is intended for maintainers only. Contributors without the secret will have these tests skipped automatically.
simplelikes follows a pragmatic REST approach. Understanding these conventions helps keep the API consistent:
| Method | When to use | Examples |
|---|---|---|
GET |
Single resource reads — no body, no side effects | GET /likes/:slug |
POST |
State-changing operations (writes) | POST /likes/:slug (toggle) |
POST (for-read) |
Batch reads requiring a body — RFC 9110 §9.3.1 discourages GET with body |
POST /likes/batch |
Sending a JSON body with GET is technically allowed by HTTP but:
- RFC 9110 §9.3.1 — body in GET has "no generally defined semantics" and is flagged as a request smuggling risk
- CDN/proxy behavior — intermediaries may strip the body or reject the connection
- Browser fetch API —
GETwith body works but is non-standard
The POST-for-read pattern is well established: Elasticsearch (POST /_search), GraphQL (POST /graphql), OData (POST /$batch), and AWS DynamoDB all use POST for reads with complex input.
For rate limiting purposes, operations are categorized by semantics, not HTTP method:
| Endpoint | Semantics | Counts against |
|---|---|---|
GET /likes/:slug |
Read | Global GET limit (500/min) |
POST /likes/:slug |
Write | Global POST limit (50/min) |
POST /likes/batch |
Batch read | Global GET limit (500/min) |
This ensures the D1 write quota (100k rows/day on free tier) is protected by the stricter POST limit, while read-heavy batch operations use the more generous GET limit.
The request pipeline evaluates limits in this order:
- Input validation — slug format (
[a-z0-9/-], max 200 chars), batch size (max 50), method - Global rate limit — per-method safeguard (500 GET/min, 50 POST/min)
- Per-IP rate limit — 10 req/min per origin IP
- D1 query — actual database read or write
- Caching — batch
cache.put()runs in background (ctx.waitUntil), GET cache is stored synchronously - CORS wrapping —
cors.wrap()clones the response and adds origin / security headers, applied after cache so both fresh and cached responses get correct CORS headers
Steps 2 and 3 return 429 Too Many Requests with Retry-After header. A failed step rejects the request before the next step runs.
Every response includes security headers. CORS is configurable via the ALLOWED_ORIGINS env var:
| Header | Preflight | Normal response |
|---|---|---|
Access-Control-Allow-Origin |
Request origin (if whitelisted) | Same |
Access-Control-Allow-Methods |
GET, POST, OPTIONS |
— |
Access-Control-Allow-Headers |
Content-Type, X-Visitor-Id |
— |
Access-Control-Max-Age |
86400 (24h) |
— |
Vary |
— | Origin |
X-Content-Type-Options |
— | nosniff |
X-Frame-Options |
— | DENY |
Default allowed origins: http://localhost:8787. For production, set ALLOWED_ORIGINS to your comma-separated domains (configured via Cloudflare dashboard or CI env vars). The CI pipeline reads ALLOWED_ORIGINS from GitHub Secrets — forks must configure their own secret or the CORS integration test will fail.
- No
PUT,PATCH, orDELETE— the API is intentionally minimal - Slugs follow
[a-z0-9/-]pattern, max 200 chars - Max 50 slugs per batch request
- Types follow
[a-z0-9-]pattern, max 50 chars."untyped"and""(empty) are reserved - All responses include CORS and security headers
- Rate limits return
Retry-Afterheader with seconds until reset
Before adding a new endpoint, ask:
- Could this be done with the existing endpoints? (e.g.,
POST /likes/batch+ individual GETs covers most read patterns) - Does it need a body / many parameters? → POST-for-read
- Does it operate on a single resource with just a path param? → GET
- Is it a write? → POST
| Workflow | Trigger | Stages |
|---|---|---|
| Build | Push to main, push to tag, workflow_dispatch |
Typecheck → Unit tests with coverage (threshold 95%) → (if push) Trigger Deploy |
| Deploy | workflow_dispatch only (called by Build or manually) |
Deploy (staging/production) → Integration tests → (if production tag) Trigger Release |
| Release | workflow_dispatch only |
Create GitHub Release from CHANGELOG |
The pipeline chain flows automatically:
Push main ──► Build ──► Deploy staging ──► Integration tests ──► done
Push tag ──► Build ──► Deploy production ──► Integration tests ──► Release
All three workflows can also be triggered manually via workflow_dispatch for reprocessing. See MAINTAINERS.md for details.
Maintainers: see MAINTAINERS.md for maintenance policies and RELEASING.md for the release process.