> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/development/coding-conventions.md).

# Coding Conventions

These are the rules of this codebase. They are not style preferences to debate per pull request — they are what reviewers check against, and what keeps a project with two runtimes and four languages readable.

## Language

{% hint style="warning" %}
**JavaScript only. No TypeScript.**
{% endhint %}

New files are `.js` or `.vue`. No `lang="ts"` on a `<script setup>` block, no `.ts` modules, no incremental TypeScript migration. A PR that introduces TypeScript will be asked to remove it.

Code comments, commit messages, and repo documentation are written in **English**. Locale packs obviously carry their own language.

## Functions

New and rewritten functions use **`const` arrow syntax**:

{% code title="the house style" %}

```js
const isValidMAC = (address) => {
    const normalized = address.replace(/[:-]/g, '');
    return normalized.length === 12 && /^[0-9A-Fa-f]+$/.test(normalized);
};

const loadSecurityChecklist = async () => { /* … */ };
```

{% endcode %}

Two caveats:

* **Object methods keep shorthand syntax.** `{ status(code) { … } }` stays as it is.
* **Arrow consts are not hoisted.** Declare them before the code that calls them.

This applies to code you write or rewrite. Do **not** mass-convert existing `function` declarations — a diff full of unrelated style churn is harder to review than the feature it hides.

## Comments

Three rules, in order of importance:

1. **Every new file opens with a header comment stating its purpose.** For an API handler that means its route and what it does. This is how the rest of the codebase is navigable without opening every file — directory-level docs deliberately stop at "read the header comments".
2. **Large templates and large functions carry block comments per meaningful region.** A 400-line `.vue` template should tell you where the input area ends and the result area starts.
3. **Comments describe the code as it is now.** No changelog narration: no "previously we did X", no "this fixes the bug where…". Git history covers the past. A comment that explains *why* a non-obvious choice was made is valuable; a comment that narrates the edit that produced it is noise.

A comment should stay shorter than the code it explains.

## Frontend conventions

Full architecture in [Frontend](/developer/architecture/frontend.md). The rules you need while writing code:

* **Composition API, `<script setup>`, everywhere.** No Options API.
* **Path alias `@` → `frontend/`.** Import as `@/utils/valid-ip.js`, never with a pile of `../`.
* **shadcn-vue first.** Check `frontend/components/ui/` for an existing primitive, then the shadcn-vue catalogue for one to copy in. Hand-rolled Tailwind is the last resort.
* **Semantic design tokens only.** `bg-info`, `bg-action`, `text-muted-foreground`, and friends — the tokens theme themselves. Never write `dark:` dual-pair utilities.
* **`console.*` is fine on the frontend.** It is banned on the backend only (see below).

### Where a helper goes

This decision comes up in almost every change, so it has a fixed answer:

<table><thead><tr><th width="200">Directory</th><th>What belongs there</th></tr></thead><tbody><tr><td><code>frontend/composables/</code></td><td>Logic that needs Vue reactivity or lifecycle. Named <code>use-xxx.js</code>, exporting <code>useXxx()</code>.</td></tr><tr><td><code>frontend/utils/</code></td><td>Framework-agnostic helpers and IO. Never <code>use-</code> prefixed.</td></tr><tr><td><code>frontend/lib/</code></td><td>shadcn support layer only — currently just <code>cn()</code>. Do not add to it.</td></tr><tr><td><code>frontend/data/</code></td><td>Static configuration and registries: tools, sections, achievements, changelog.</td></tr></tbody></table>

One refinement: **a pure function that lives next to a composable is exported from that composable's file**, not promoted to its own module in `utils/`. `ipFieldTone()` shipping out of `composables/use-status-tone.js` is the pattern to copy.

## Shared code lives in `common/`

Anything both halves need goes in `common/` — the single source of truth — and the frontend reaches it through a **thin re-export bridge** in `utils/`, so frontend imports keep their familiar `@/utils/...` shape:

{% code title="frontend/utils/valid-ip.js" %}

```js
// Single source of truth is common/valid-ip.js (shared with the backend).
// This file exists as a thin re-export so front-end code can keep writing
// `import { isValidIP } from '@/utils/valid-ip.js'` without caring where
// the implementation lives.
export { isValidIP, isIPv6, isValidDomain } from '../../common/valid-ip.js';
```

{% endcode %}

`frontend/utils/fetch-with-timeout.js` follows the same pattern. When you add a bridge, add a spec that imports **both** paths and asserts they agree — `tests/valid-ip.test.js` does exactly this, which is what keeps a bridge from quietly growing a second implementation.

## Backend conventions

Full picture in [Backend](/developer/architecture/backend.md). The rules that bite:

### Handler shape

One file per route under `api/`, with a single default export:

```js
export default async (req, res) => {
    // read req.query / req.body, call upstream, write one response
};
```

Error shape is terse and consistent: `400` on bad input, `res.status(500).json({ error: error.message })` on upstream failure. The frontend does not display these verbatim.

### Never a bare `fetch()`

Every outbound HTTP call from `api/` goes through `fetchUpstream` from `common/fetch-with-timeout.js`. It applies an 8-second timeout and a default `User-Agent`. A hanging upstream provider must time out, not pin the connection open.

### Guards, not inline checks

Access control and parameter validation live in middleware (`common/guards.js`), attached in `backend-server.js`. Handlers never repeat them:

* `requireReferer` — global on `/api/*`
* `requireValidIP()` / `requireValidDomain()` / `requireValidPrefix()` / `requireValidASN()` / `requireValidProviderId()` / `requireValidReportId()` — per route

A new parameter shape means a new guard in `common/guards.js`, not an open-coded check at the top of a handler.

### Logging

{% hint style="danger" %}
**`console.*` is banned in backend files.** Always the shared pino logger from `common/logger.js`.
{% endhint %}

Pino is context-first — the object comes first, the short message second:

{% code title="the house style" %}

```js
import logger from '../common/logger.js';

logger.error({ err: e, mac: macAddress }, 'mac-checker handler failed');
logger.warn({ err: e, query }, 'whois: RDAP IP lookup failed, trying WHOIS');
```

{% endcode %}

Two more rules:

* **Handlers never log "received request" lines.** Per-request logging is `pino-http`'s job, mounted on `/api` only when `LOG_HTTP=true`.
* **Startup-only lines lead with an emoji** — 🚀 listening, 📦 ready, 📥 downloading, 🛡️ security, 🐢 throttling, 🗓️ schedule, ⚠️ recoverable, ❌ failure. Per-request logs stay plain.

Configuration is `LOG_LEVEL` (default `info`), `LOG_FORMAT=json` for log shippers, and `LOG_HTTP=true`. There is no `NODE_ENV` anywhere in this project — see [Logging](/developer/configuration/logging.md).

### Edge caching

Every `/api/*` response defaults to `Cache-Control: no-store`. Slowly-changing public routes opt in via the `cacheable(maxAgeSeconds)` middleware in `backend-server.js`. Write TTLs as multiplied expressions (`24 * 60 * 60`), not raw seconds. Handlers themselves never touch `Cache-Control`, and per-user or authenticated endpoints are never wrapped.

## What ships with your change

Two things are not optional, and both are enforced:

* **i18n coverage.** Anything that surfaces copy lands in **all four locales** (`en` / `zh` / `fr` / `ru`) in the same change, including the `frontend/data/changelog.json` entry. See [i18n](/developer/development/i18n.md).
* **Tests.** Any non-visual logic exercisable without a network call ships with a spec in `tests/`, in the same change. Update affected specs when behavior shifts — don't defer. See [Testing](/developer/development/testing.md).

Then run `pnpm check`. It must be green before you hand the change off.

## Next

* [Adding a New Tool](/developer/development/adding-a-new-tool.md) — all of the above, applied end to end.
* [How to Contribute](/developer/contributing/how-to-contribute.md) — branches, commits, and PR expectations.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/development/coding-conventions.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
