# API Best Practices

Practical guidance for working with the API in production

***

### 1. Parsing the Response Correctly <a href="#id-1-parsing-the-response-correctly" id="id-1-parsing-the-response-correctly"></a>

This is the most critical thing to get right. The API response is **not** a simple array of job objects.

#### The Problem <a href="#the-problem" id="the-problem"></a>

The API returns a top-level JSON array that contains **mixed types** — typically two strings followed by the actual jobs array:

```json
["string_value", "another_string", [{ "title": "...", ... }, { "title": "...", ... }]]
```

If you naively treat the response as `Job[]`, you'll get strings instead of job objects.

#### The Solution <a href="#the-solution" id="the-solution"></a>

Search for the nested array within the root array:

```javascript
const response = await fetch(url);
const data = await response.json();

if (!Array.isArray(data)) {
  throw new Error('Unexpected response format');
}

// Find the nested array containing job objects
let jobs = data.find(item => Array.isArray(item));

// Fallback: if every element is an object, the root array IS the jobs array
if (!jobs && data.length > 0 && typeof data[0] === 'object' && data[0] !== null) {
  jobs = data;
}

if (!jobs || !Array.isArray(jobs)) {
  throw new Error('Could not locate jobs array in response');
}
```

#### Python Equivalent <a href="#python-equivalent" id="python-equivalent"></a>

```python
import requests

resp = requests.get(url, params=params)
data = resp.json()

if not isinstance(data, list):
    raise ValueError("Unexpected response format")

jobs = next((item for item in data if isinstance(item, list)), None)

if jobs is None and len(data) > 0 and isinstance(data[0], dict):
    jobs = data

if not jobs:
    raise ValueError("Could not locate jobs array in response")
```

> **Do not assume the response structure is stable.** The mixed-type root array is the current behaviour, but defensive parsing that searches for the jobs array will survive format changes.

***

### 2. Handling HTML in Descriptions <a href="#id-2-handling-html-in-descriptions" id="id-2-handling-html-in-descriptions"></a>

Job descriptions frequently contain raw HTML markup:

```html
<p>We are looking for a <strong>Senior Solidity Developer</strong> to join our team.</p>
<ul>
  <li>5+ years of experience</li>
  <li>Knowledge of ERC-20 and ERC-721 standards</li>
</ul>
```

#### Strip HTML for Plain Text <a href="#strip-html-for-plain-text" id="strip-html-for-plain-text"></a>

```javascript
function stripHtml(html) {
  return html
    .replace(/<[^>]+>/g, ' ')  // Replace tags with spaces
    .replace(/\s+/g, ' ')       // Collapse whitespace
    .trim();
}
```

#### Truncate Long Descriptions <a href="#truncate-long-descriptions" id="truncate-long-descriptions"></a>

Descriptions can be very long. If you're displaying summaries or working within token limits (e.g., AI/LLM contexts), truncate after stripping:

```javascript
function cleanDescription(html, maxLength = 500) {
  const text = stripHtml(html);
  if (text.length > maxLength) {
    return text.substring(0, maxLength) + '...';
  }
  return text;
}
```

#### When to Disable Descriptions <a href="#when-to-disable-descriptions" id="when-to-disable-descriptions"></a>

Use `show_description=false` when:

* Building a job listing/browse view (titles + companies are enough)
* Doing an initial scan before fetching details
* Working within bandwidth or payload size constraints
* The consumer doesn't need description text (e.g., job count analytics)

***

### 3. Caching <a href="#id-3-caching" id="id-3-caching"></a>

The API returns recent job listings that don't change by the second. Caching responses for **5 minutes** is a sensible default — it reduces API calls significantly while keeping data fresh enough for most use cases.

#### Simple In-Memory Cache <a href="#simple-in-memory-cache" id="simple-in-memory-cache"></a>

```javascript
class Cache {
  constructor(ttlMs = 5 * 60 * 1000) {
    this.store = new Map();
    this.ttl = ttlMs;
  }

  get(key) {
    const entry = this.store.get(key);
    if (!entry) return undefined;
    if (Date.now() > entry.expiry) {
      this.store.delete(key);
      return undefined;
    }
    return entry.value;
  }

  set(key, value) {
    this.store.set(key, { value, expiry: Date.now() + this.ttl });
  }
}
```

#### Cache Key Strategy <a href="#cache-key-strategy" id="cache-key-strategy"></a>

Use the full set of filter parameters as the cache key. Serialise them deterministically:

```javascript
const cacheKey = JSON.stringify({ tag, country, remote, limit, show_description });
```

This ensures that `?tag=solidity&limit=10` and `?tag=solidity&limit=20` are cached separately.

#### When to Cache More Aggressively <a href="#when-to-cache-more-aggressively" id="when-to-cache-more-aggressively"></a>

* **Static reference data** (like the list of available tags) can be cached indefinitely or for hours
* **Broad, unfiltered queries** are expensive and change slowly — cache for 10+ minutes
* **Highly specific queries** (tag + country + remote) return smaller datasets that change more often — 5 minutes is appropriate

***

### 4. Rate Limiting and Retry Logic <a href="#id-4-rate-limiting-and-retry-logic" id="id-4-rate-limiting-and-retry-logic"></a>

The API enforces rate limits and returns `429 Too Many Requests` when exceeded.

#### Exponential Backoff with Jitter <a href="#exponential-backoff-with-jitter" id="exponential-backoff-with-jitter"></a>

```javascript
async function fetchWithRetry(url, params, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url + '?' + new URLSearchParams(params));
      if (response.ok) return response;

      if (response.status === 429 || response.status >= 500) {
        if (attempt === maxRetries) throw new Error(`Failed after ${maxRetries} retries`);

        const baseDelay = Math.min(1000 * Math.pow(2, attempt), 10000);
        const jitter = baseDelay * 0.2 * (Math.random() * 2 - 1);  // +/- 20%
        await new Promise(r => setTimeout(r, baseDelay + jitter));
        continue;
      }

      throw new Error(`API error: ${response.status}`);
    } catch (err) {
      if (attempt === maxRetries) throw err;
      // Retry on network errors too
      const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}
```

#### What to Retry <a href="#what-to-retry" id="what-to-retry"></a>

| Error                         | Retry?  | Why                               |
| ----------------------------- | ------- | --------------------------------- |
| `429 Too Many Requests`       | **Yes** | Temporary rate limit — will clear |
| `5xx Server Error`            | **Yes** | Transient server issues           |
| Network timeout / no response | **Yes** | Connectivity blip                 |
| `401 Unauthorized`            | **No**  | Bad token — retrying won't help   |
| `403 Forbidden`               | **No**  | Permissions issue                 |
| `4xx` (other)                 | **No**  | Client error — fix the request    |

#### Recommended Retry Config <a href="#recommended-retry-config" id="recommended-retry-config"></a>

| Setting       | Value      | Rationale                                                         |
| ------------- | ---------- | ----------------------------------------------------------------- |
| Max retries   | 3          | Enough to ride out transient issues without hammering the API     |
| Initial delay | 1 second   | Gives the rate limiter time to reset                              |
| Max delay     | 10 seconds | Caps wait time to keep UX responsive                              |
| Jitter        | +/- 20%    | Prevents thundering herd if multiple clients retry simultaneously |

***

### 5. Input Validation <a href="#id-5-input-validation" id="id-5-input-validation"></a>

The API silently returns empty results for invalid filter values. Validate on the client side to catch mistakes early.

#### Validate Tags <a href="#validate-tags" id="validate-tags"></a>

```javascript
const VALID_TAGS = [
  "ai", "analyst", "backend", "bitcoin", "blockchain", "community-manager",
  "crypto", "cryptography", "cto", "customer-support", "dao", "data-science",
  "defi", "design", "developer-relations", "devops", "discord", "economy-designer",
  "entry-level", "erc", "erc-20", "evm", "front-end", "full-stack", "gaming",
  "ganache", "golang", "hardhat", "intern", "java", "javascript", "layer-2",
  "marketing", "mobile", "moderator", "nft", "node", "non-tech", "open-source",
  "openzeppelin", "pay-in-crypto", "product-manager", "project-manager",
  "react", "refi", "research", "ruby", "rust", "sales", "smart-contract",
  "solana", "solidity", "truffle", "web3-py", "web3js", "zero-knowledge"
];

function isValidTag(tag) {
  return VALID_TAGS.includes(tag.toLowerCase());
}
```

#### Validate Country Slugs <a href="#validate-country-slugs" id="validate-country-slugs"></a>

```javascript
function isValidCountrySlug(slug) {
  // Must be lowercase, may contain hyphens, no spaces or special chars
  return /^[a-z]+(-[a-z]+)*$/.test(slug);
}
```

#### Common Mistakes to Catch <a href="#common-mistakes-to-catch" id="common-mistakes-to-catch"></a>

| User Input          | Problem           | Correct Value      |
| ------------------- | ----------------- | ------------------ |
| `"Solidity"`        | Capitalised       | `"solidity"`       |
| `"react developer"` | Multi-word phrase | `"react"`          |
| `"web3 marketing"`  | Multi-word phrase | `"marketing"`      |
| `"USA"`             | Not a slug        | `"united-states"`  |
| `"UK"`              | Not a slug        | `"united-kingdom"` |
| `"solidty"`         | Typo              | `"solidity"`       |

***

### 6. Handling Optional Fields <a href="#id-6-handling-optional-fields" id="id-6-handling-optional-fields"></a>

Job objects have inconsistent field presence. Not every job has every field. Always code defensively:

```javascript
// Defensive field access
const title = job.title || 'Untitled Position';
const salary = job.salary || 'Not disclosed';
const tags = Array.isArray(job.tags) ? job.tags : [];
const isRemote = job.remote === true;
```

#### Extra Fields <a href="#extra-fields" id="extra-fields"></a>

Job objects may contain fields beyond the documented schema. The API can return additional metadata. Use a pass-through pattern to preserve these:

```javascript
// Preserve all fields, even unexpected ones
const processed = {
  id: job.id,
  title: job.title,
  company: job.company,
  // ... known fields ...
};

// Copy any extra fields
Object.keys(job).forEach(key => {
  if (!(key in processed)) {
    processed[key] = job[key];
  }
});
```

***

### 7. Efficient Multi-Tag Searching <a href="#id-7-efficient-multi-tag-searching" id="id-7-efficient-multi-tag-searching"></a>

Since the API only supports one tag per request, searching across multiple tags requires parallel requests.

#### Parallel Fetching with Deduplication <a href="#parallel-fetching-with-deduplication" id="parallel-fetching-with-deduplication"></a>

```javascript
async function searchMultipleTags(tags, baseFilters = {}) {
  const results = await Promise.all(
    tags.map(tag => fetchJobs({ ...baseFilters, tag }))
  );

  // Deduplicate by URL (most reliable unique identifier)
  const seen = new Set();
  const unique = [];

  for (const jobs of results) {
    for (const job of jobs) {
      const key = job.url || job.id || JSON.stringify(job);
      if (!seen.has(key)) {
        seen.add(key);
        unique.push(job);
      }
    }
  }

  return unique;
}

// Usage: find any engineering job
const engineeringJobs = await searchMultipleTags(
  ['backend', 'front-end', 'full-stack', 'smart-contract'],
  { remote: true, limit: 30 }
);
```

> **Be mindful of rate limits** when making parallel requests. If you're searching across many tags, consider batching with delays between groups.

***

### 8. Optimising Payload Size <a href="#id-8-optimising-payload-size" id="id-8-optimising-payload-size"></a>

Full responses with descriptions can be large. Optimise based on your use case:

| Scenario             | `show_description` | `limit` | Why                                                |
| -------------------- | ------------------ | ------- | -------------------------------------------------- |
| Job listing page     | `false`            | 50-100  | Titles + companies are enough for browsing         |
| Job detail view      | `true`             | 1-5     | Only fetch descriptions for jobs the user selected |
| AI agent summary     | `true`             | 10-20   | AI needs descriptions for context, but limit count |
| Analytics / counting | `false`            | 100     | Maximise data, minimise bandwidth                  |
| Alert / notification | `false`            | 10      | Quick check for new postings                       |

***

### 9. Error Handling Checklist <a href="#id-9-error-handling-checklist" id="id-9-error-handling-checklist"></a>

A robust integration should handle all of these:

* \[ ] **401** — Token is wrong or expired. Surface a clear message pointing to the token page.
* \[ ] **403** — Token permissions issue. Direct user to check their account.
* \[ ] **429** — Rate limit. Implement backoff + retry. Surface a "please wait" message if retry fails.
* \[ ] **5xx** — Server down. Retry with backoff. Show cached data if available.
* \[ ] **Network error** — No response received. Retry, then surface connectivity message.
* \[ ] **Empty results** — Not an error, but possibly an invalid filter. Suggest checking tag/country spelling.
* \[ ] **Unexpected response format** — The response shape changed. Log the raw response for debugging.
* \[ ] **Missing fields on jobs** — Gracefully handle `undefined` for any field.
* \[ ] **HTML in descriptions** — Strip or render depending on context.

***

### 10. Security Considerations <a href="#id-10-security-considerations" id="id-10-security-considerations"></a>

* **Never commit your API token** to version control. Use environment variables.
* **Never log the full request URL** in production — it contains your token as a query parameter.
* **Add `.env` and `.env.local` to `.gitignore`** if storing tokens in env files.
* **Rotate tokens** if you suspect they've been exposed (check web3.career account settings).
* If building a multi-user service, **never share tokens between users** — each user should authenticate with their own token.
