Back to skills
SkillHub ClubResearch & OpsFull StackBackendData / AI

netbox-integration-best-practices

Best practices for building integrations with NetBox REST and GraphQL APIs. Use when building NetBox API integrations, reviewing integration code, troubleshooting NetBox performance issues, planning automation architecture, writing scripts that interact with NetBox, using pynetbox, configuring Diode for data ingestion, or implementing NetBox webhooks.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
21
Hot score
87
Updated
March 20, 2026
Overall rating
C1.8
Composite score
1.8
Best-practice grade
B73.6

Install command

npx @skill-hub/cli install netboxlabs-netbox-best-practices-netbox-integration-best-practices

Repository

netboxlabs/netbox-best-practices

Skill path: skills/netbox-integration-best-practices

Best practices for building integrations with NetBox REST and GraphQL APIs. Use when building NetBox API integrations, reviewing integration code, troubleshooting NetBox performance issues, planning automation architecture, writing scripts that interact with NetBox, using pynetbox, configuring Diode for data ingestion, or implementing NetBox webhooks.

Open repository

Best for

Primary workflow: Research & Ops.

Technical facets: Full Stack, Backend, Data / AI, Tech Writer, Integration.

Target audience: everyone.

License: Apache-2.0.

Original source

Catalog source: SkillHub Club.

Repository owner: netboxlabs.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install netbox-integration-best-practices into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/netboxlabs/netbox-best-practices before adding netbox-integration-best-practices to shared team environments
  • Use netbox-integration-best-practices for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: netbox-integration-best-practices
description: Best practices for building integrations with NetBox REST and GraphQL APIs. Use when building NetBox API integrations, reviewing integration code, troubleshooting NetBox performance issues, planning automation architecture, writing scripts that interact with NetBox, using pynetbox, configuring Diode for data ingestion, or implementing NetBox webhooks.
license: Apache-2.0
---

# NetBox Integration Best Practices Skill

This skill provides best practices guidance for building integrations and automations with NetBox REST and GraphQL APIs.

## Target Audience

- Engineers building integrations atop NetBox APIs
- Teams planning new automations with Claude
- Developers learning NetBox API best practices

**Scope:** This skill covers API integration patterns. It does NOT cover plugin development, custom scripts, or NetBox administration.

## NetBox Version Requirements

| Feature | Version Required |
|---------|-----------------|
| REST API | All versions |
| GraphQL API | 2.9+ |
| v2 Tokens | 4.5+ (use these!) |
| v1 Token Deprecation | 4.7+ (migrate before this) |

**Primary target:** NetBox 4.4+ with 4.5+ for v2 token features.

## When to Apply This Skill

Apply these practices when:
- Building new NetBox API integrations
- Reviewing existing integration code
- Troubleshooting performance issues
- Planning automation architecture
- Writing scripts that interact with NetBox

## Priority Levels

| Level | Description | Action |
|-------|-------------|--------|
| **CRITICAL** | Security vulnerabilities, data loss, severe performance | Must fix immediately |
| **HIGH** | Significant performance/reliability impact | Should fix soon |
| **MEDIUM** | Notable improvements, best practices | Plan to address |
| **LOW** | Minor improvements, optional | Consider when convenient |

## Quick Reference

### Authentication
- **Use v2 tokens** on NetBox 4.5+: `Bearer nbt_<key>.<token>`
- **Migrate from v1** before NetBox 4.7 (deprecation planned)

### REST API
- **Always paginate**: `?limit=100` (max 1000)
- **Use PATCH** for partial updates, not PUT
- **Use ?brief=True** for list operations
- **Exclude config_context**: `?exclude=config_context` (major performance impact)
- **Avoid ?q=** search filter at scale; use specific filters
- **Bulk operations** use list endpoints with JSON arrays (not separate endpoints)

### GraphQL
- **Use the query optimizer**: [netbox-graphql-query-optimizer](https://github.com/netboxlabs/netbox-graphql-query-optimizer)
- **Always paginate** every list query
- **Paginate at every level** of nesting
- **Beware offset pagination at scale**: Deep offsets are slow; use ID range filtering in 4.5.x, cursor-based in 4.6.0+ ([#21110](https://github.com/netbox-community/netbox/issues/21110))
- **Request only needed fields**
- **Keep depth ≤3**, never exceed 5

### Performance
- Exclude config_context from device lists
- Use brief mode for large lists
- Parallelize independent requests

### Data Ingestion (Diode)
- For high-volume data ingestion, use [Diode](https://github.com/netboxlabs/diode) instead of direct API
- **Specify dependencies by name**, not ID—Diode resolves or creates them
- **No dependency order needed**—Diode handles object creation order
- Use `pip install netboxlabs-diode-sdk` for Python
- Use REST/GraphQL API for reading; use Diode for writing/populating

### Branching (Plugin)
> Requires [netbox-branching](https://github.com/netboxlabs/netbox-branching) plugin.

- **Lifecycle**: Create → Wait (PROVISIONING→READY) → Work → Sync → Merge
- **Context header**: `X-NetBox-Branch: {schema_id}` (8-char ID, not name)
- **Async operations**: sync/merge/revert return Job objects—poll for completion
- **Dry-run**: All async ops accept `{"commit": false}` for validation

## Rules by Category

### Authentication Rules

| Rule | Impact | Description |
|------|--------|-------------|
| [auth-use-v2-tokens](./references/rules/auth-use-v2-tokens.md) | CRITICAL | Use v2 tokens on NetBox 4.5+ |
| [auth-provisioning-endpoint](./references/rules/auth-provisioning-endpoint.md) | MEDIUM | Use provisioning endpoint for automated token creation |

### REST API Rules

| Rule | Impact | Description |
|------|--------|-------------|
| [rest-list-endpoint-bulk-ops](./references/rules/rest-list-endpoint-bulk-ops.md) | CRITICAL | Use list endpoints for bulk operations |
| [rest-pagination-required](./references/rules/rest-pagination-required.md) | HIGH | Always paginate list requests |
| [rest-patch-vs-put](./references/rules/rest-patch-vs-put.md) | HIGH | Use PATCH for partial updates |
| [rest-brief-mode](./references/rules/rest-brief-mode.md) | HIGH | Use ?brief=True for lists |
| [rest-field-selection](./references/rules/rest-field-selection.md) | HIGH | Use ?fields= to select fields |
| [rest-exclude-config-context](./references/rules/rest-exclude-config-context.md) | HIGH | Exclude config_context from device lists |
| [rest-avoid-search-filter-at-scale](./references/rules/rest-avoid-search-filter-at-scale.md) | HIGH | Avoid q= with large datasets |
| [rest-filtering-expressions](./references/rules/rest-filtering-expressions.md) | MEDIUM | Use lookup expressions |
| [rest-custom-field-filters](./references/rules/rest-custom-field-filters.md) | MEDIUM | Filter by custom fields |
| [rest-nested-serializers](./references/rules/rest-nested-serializers.md) | LOW | Understand nested serializers |
| [rest-ordering-results](./references/rules/rest-ordering-results.md) | LOW | Use ordering parameter |
| [rest-options-discovery](./references/rules/rest-options-discovery.md) | LOW | Use OPTIONS for discovery |

### GraphQL Rules

| Rule | Impact | Description |
|------|--------|-------------|
| [graphql-use-query-optimizer](./references/rules/graphql-use-query-optimizer.md) | CRITICAL | Use query optimizer |
| [graphql-always-paginate](./references/rules/graphql-always-paginate.md) | CRITICAL | Paginate every list query |
| [graphql-pagination-at-each-level](./references/rules/graphql-pagination-at-each-level.md) | HIGH | Paginate nested lists |
| [graphql-select-only-needed](./references/rules/graphql-select-only-needed.md) | HIGH | Request only needed fields |
| [graphql-calibrate-optimizer](./references/rules/graphql-calibrate-optimizer.md) | HIGH | Calibrate against production |
| [graphql-max-depth](./references/rules/graphql-max-depth.md) | HIGH | Keep depth ≤3 |
| [graphql-prefer-filters](./references/rules/graphql-prefer-filters.md) | MEDIUM | Filter server-side |
| [graphql-vs-rest-decision](./references/rules/graphql-vs-rest-decision.md) | MEDIUM | Choose appropriate API |
| [graphql-complexity-budgets](./references/rules/graphql-complexity-budgets.md) | LOW | Establish complexity budgets |

### Performance Rules

| Rule | Impact | Description |
|------|--------|-------------|
| [perf-exclude-config-context](./references/rules/perf-exclude-config-context.md) | HIGH | Exclude config_context |
| [perf-brief-mode-lists](./references/rules/perf-brief-mode-lists.md) | HIGH | Use brief mode for lists |

### Data Modeling Rules

| Rule | Impact | Description |
|------|--------|-------------|
| [data-dependency-order](./references/rules/data-dependency-order.md) | CRITICAL | Create objects in dependency order |
| [data-site-hierarchy](./references/rules/data-site-hierarchy.md) | MEDIUM | Understand site hierarchy |
| [data-ipam-hierarchy](./references/rules/data-ipam-hierarchy.md) | MEDIUM | Understand IPAM hierarchy |
| [data-custom-fields](./references/rules/data-custom-fields.md) | MEDIUM | Use custom fields properly |
| [data-tags-usage](./references/rules/data-tags-usage.md) | MEDIUM | Use tags for classification |
| [data-tenant-isolation](./references/rules/data-tenant-isolation.md) | MEDIUM | Use tenants for separation |
| [data-natural-keys](./references/rules/data-natural-keys.md) | MEDIUM | Use natural keys |

### Integration Rules

| Rule | Impact | Description |
|------|--------|-------------|
| [integ-diode-ingestion](./references/rules/integ-diode-ingestion.md) | HIGH | Use Diode for high-volume data ingestion |
| [integ-pynetbox-client](./references/rules/integ-pynetbox-client.md) | HIGH | Use pynetbox for Python |
| [integ-branch-api-workflow](./references/rules/integ-branch-api-workflow.md) | HIGH | Complete branching lifecycle (plugin) |
| [integ-branch-context-header](./references/rules/integ-branch-context-header.md) | HIGH | Branch context with X-NetBox-Branch header (plugin) |
| [integ-branch-async-operations](./references/rules/integ-branch-async-operations.md) | MEDIUM | Job polling for sync/merge/revert (plugin) |
| [integ-webhook-configuration](./references/rules/integ-webhook-configuration.md) | MEDIUM | Configure webhooks |
| [integ-change-tracking](./references/rules/integ-change-tracking.md) | LOW | Query object changes |

## External References

### Official Documentation
- [NetBox Documentation](https://netboxlabs.com/docs/netbox/en/stable/)
- [REST API Guide](https://netboxlabs.com/docs/netbox/en/stable/integrations/rest-api/)
- [GraphQL API Guide](https://netboxlabs.com/docs/netbox/en/stable/integrations/graphql-api/)

### Essential Tools
- [pynetbox](https://github.com/netbox-community/pynetbox) - Official Python client
- [netbox-graphql-query-optimizer](https://github.com/netboxlabs/netbox-graphql-query-optimizer) - Query analysis (essential for GraphQL)
- [Diode](https://github.com/netboxlabs/diode) - Data ingestion service (for high-volume writes)
- [Diode Python SDK](https://github.com/netboxlabs/diode-sdk-python) - Python client for Diode
- [NetBox Branching](https://github.com/netboxlabs/netbox-branching) - Change management plugin (optional)

### Community
- [NetBox GitHub](https://github.com/netbox-community/netbox)
- [NetBox Discussions](https://github.com/netbox-community/netbox/discussions)

## Reference Documentation

| Document | Purpose |
|----------|---------|
| [HUMAN.md](../../HUMAN.md) | Human-readable guide for engineers |
| [netbox-integration-guidelines.md](./references/netbox-integration-guidelines.md) | Comprehensive technical reference |


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/rules/auth-use-v2-tokens.md

```markdown
---
title: Use v2 Tokens on NetBox 4.5+
impact: CRITICAL
category: auth
tags: [authentication, tokens, security, migration]
netbox_version: "4.5+"
---

# auth-use-v2-tokens: Use v2 Tokens on NetBox 4.5+

## Rationale

NetBox 4.5 introduced v2 tokens with significant security improvements. v1 tokens store plaintext secrets in the database, creating risk if the database is compromised. v2 tokens use HMAC-SHA256 hashing with a pepper, ensuring the plaintext token is never stored.

**Timeline:**
- NetBox 4.5.0: v2 tokens introduced
- NetBox 4.7.0: v1 tokens deprecated (removal planned)

Migrating to v2 tokens before 4.7 is essential for uninterrupted API access.

## Incorrect Pattern

```python
# WRONG: v1 token format (deprecated in 4.7+)
import requests

NETBOX_URL = "https://netbox.example.com"
TOKEN = "0123456789abcdef0123456789abcdef01234567"

headers = {
    "Authorization": f"Token {TOKEN}",  # v1 format
    "Content-Type": "application/json"
}

response = requests.get(f"{NETBOX_URL}/api/dcim/devices/", headers=headers)
```

**Problems with this approach:**
- v1 tokens are stored in plaintext in the database
- Database compromise exposes all tokens
- Will stop working after v1 removal (post-4.7)
- No cryptographic protection of token secrets

## Correct Pattern

```python
# CORRECT: v2 token format (NetBox 4.5+)
import requests

NETBOX_URL = "https://netbox.example.com"
TOKEN = "nbt_abc123def456.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

headers = {
    "Authorization": f"Bearer {TOKEN}",  # v2 format
    "Content-Type": "application/json"
}

response = requests.get(f"{NETBOX_URL}/api/dcim/devices/", headers=headers)
```

**Benefits:**
- Token secret hashed with HMAC-SHA256
- Pepper adds server-side secret to hash
- Database compromise doesn't expose usable tokens
- Future-proof for NetBox 4.7+

## v2 Token Format Details

```
Bearer nbt_<key>.<token>
```

- `Bearer`: OAuth-style prefix (required)
- `nbt_`: NetBox token identifier
- `<key>`: Public key for token lookup
- `<token>`: Secret portion (never stored in plaintext)

## pynetbox Example

```python
import pynetbox

# pynetbox handles the header format automatically
nb = pynetbox.api(
    url="https://netbox.example.com",
    token="nbt_abc123def456.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
)

# Works with both v1 and v2 tokens
devices = nb.dcim.devices.all()
```

## Migration Steps

1. **Check NetBox version:** Ensure you're on 4.5+
2. **Verify API_TOKEN_PEPPERS configured:** Required for v2 tokens
3. **Generate new v2 token** in NetBox UI
4. **Update integration** with new token format
5. **Test thoroughly** before production
6. **Revoke old v1 token** after successful migration

## Server Configuration Requirement

v2 tokens require `API_TOKEN_PEPPERS` in NetBox configuration:

```python
# configuration.py
API_TOKEN_PEPPERS = {
    'default': 'your-secret-pepper-minimum-32-characters-long',
}
```

Without this, v2 tokens cannot be validated.

## Exceptions

- **NetBox < 4.5:** v1 tokens are the only option
- **Legacy integrations:** May require coordinated migration across teams

## Related Rules

- [auth-token-rotation](./auth-token-rotation.md) - Implement regular token rotation
- [sec-token-storage](./sec-token-storage.md) - Never store tokens in code

## References

- [NetBox 4.5 Release Notes](https://netboxlabs.com/docs/netbox/en/stable/release-notes/)
- [NetBox Authentication](https://netboxlabs.com/docs/netbox/en/stable/integrations/rest-api/#authentication)

```

### references/rules/auth-provisioning-endpoint.md

```markdown
---
title: Use Provisioning Endpoint for Automated Token Creation
impact: MEDIUM
category: auth
tags: [authentication, tokens, automation, provisioning]
netbox_version: "4.4+"
---

# auth-provisioning-endpoint: Use Provisioning Endpoint for Automated Token Creation

## Rationale

For systems that need to bootstrap their own tokens, NetBox provides a token provisioning endpoint. This enables automated token creation without pre-existing API access.

## Correct Pattern

```python
import requests

def provision_token(netbox_url, username, password, description=None):
    """Provision a new API token using credentials."""
    payload = {
        "username": username,
        "password": password
    }
    if description:
        payload["description"] = description

    response = requests.post(
        f"{netbox_url}/api/users/tokens/provision/",
        json=payload,
        headers={"Content-Type": "application/json"}
    )

    if response.status_code == 201:
        return response.json()["key"]
    else:
        raise Exception(f"Failed to provision token: {response.text}")

# Usage
token = provision_token(
    "https://netbox.example.com",
    "automation-user",
    "secure-password",
    "Automated pipeline token"
)
# Store token securely for future use
```

## Use Cases

- CI/CD pipeline bootstrapping
- Dynamic environment provisioning
- Token rotation automation
- Multi-tenant token management

## Security Considerations

- Protect the username/password used for provisioning
- Use service accounts with limited permissions
- Consider provisioning tokens with short lifespans

## Related Rules

- [auth-token-rotation](./auth-token-rotation.md) - Rotate tokens regularly
- [sec-token-storage](./sec-token-storage.md) - Store tokens securely

```

### references/rules/rest-list-endpoint-bulk-ops.md

```markdown
---
title: Use List Endpoints for Bulk Operations
impact: CRITICAL
category: rest
tags: [rest, bulk, create, update, delete, transactions]
netbox_version: "4.4+"
---

# rest-list-endpoint-bulk-ops: Use List Endpoints for Bulk Operations

## Rationale

NetBox does NOT have separate "bulk endpoints." Instead, bulk create, update, and delete operations use the standard list endpoints with JSON arrays. This is a common source of confusion.

Key characteristics:
- **Atomic:** All items succeed or all fail (transaction rollback)
- **Efficient:** Single HTTP request for multiple objects
- **Validated:** Each item is fully validated before commit
- **Signal-safe:** Triggers Django signals and webhooks for each object

Understanding this pattern is essential for efficient data population and management.

## Incorrect Pattern

```python
# WRONG: Creating objects one at a time
import requests

devices_to_create = [
    {"name": "switch-01", "device_type": 1, "role": 1, "site": 1},
    {"name": "switch-02", "device_type": 1, "role": 1, "site": 1},
    {"name": "switch-03", "device_type": 1, "role": 1, "site": 1},
]

# Inefficient: 3 HTTP requests
for device in devices_to_create:
    response = requests.post(
        f"{API_URL}/dcim/devices/",
        headers=headers,
        json=device  # Single object
    )
```

```python
# WRONG: Looking for non-existent bulk endpoint
response = requests.post(
    f"{API_URL}/dcim/devices/bulk-create/",  # This endpoint doesn't exist!
    headers=headers,
    json=devices_to_create
)
```

**Problems with this approach:**
- Multiple HTTP requests (network overhead)
- No atomicity (partial success possible)
- Slower overall execution
- Looking for endpoints that don't exist

## Correct Pattern

### Bulk Create (POST array to list endpoint)

```python
import requests

API_URL = "https://netbox.example.com/api"
headers = {
    "Authorization": "Bearer nbt_abc123.xxxxx",
    "Content-Type": "application/json"
}

# Create multiple devices in one request
devices = [
    {"name": "switch-01", "device_type": 1, "role": 1, "site": 1, "status": "active"},
    {"name": "switch-02", "device_type": 1, "role": 1, "site": 1, "status": "active"},
    {"name": "switch-03", "device_type": 1, "role": 1, "site": 1, "status": "planned"},
]

response = requests.post(
    f"{API_URL}/dcim/devices/",  # Regular list endpoint
    headers=headers,
    json=devices  # JSON array, not single object
)

if response.status_code == 201:
    created = response.json()  # Returns array of created objects
    for device in created:
        print(f"Created: {device['name']} (ID: {device['id']})")
else:
    print(f"Error: {response.json()}")
```

### Bulk Update (PATCH array to list endpoint)

```python
# Update multiple devices - each object MUST include "id"
updates = [
    {"id": 1, "status": "active"},
    {"id": 2, "status": "active"},
    {"id": 3, "status": "staged"},
]

response = requests.patch(
    f"{API_URL}/dcim/devices/",  # Same list endpoint
    headers=headers,
    json=updates  # Array with "id" in each object
)

if response.status_code == 200:
    updated = response.json()  # Returns array of updated objects
    for device in updated:
        print(f"Updated: {device['name']} -> {device['status']}")
```

### Bulk Delete (DELETE array to list endpoint)

```python
# Delete multiple devices
deletions = [
    {"id": 1},
    {"id": 2},
    {"id": 3},
]

response = requests.delete(
    f"{API_URL}/dcim/devices/",  # Same list endpoint
    headers=headers,
    json=deletions  # Array of {"id": X} objects
)

if response.status_code == 204:
    print("All devices deleted successfully")
```

## Atomicity Behavior

**All bulk operations are atomic (all-or-none):**

```python
# If any item fails validation, entire operation is rolled back
devices = [
    {"name": "switch-01", "device_type": 1, "role": 1, "site": 1},  # Valid
    {"name": "switch-02", "device_type": 1, "role": 1, "site": 1},  # Valid
    {"name": "", "device_type": 1, "role": 1, "site": 1},           # INVALID: empty name
]

response = requests.post(f"{API_URL}/dcim/devices/", headers=headers, json=devices)

# Response: 400 Bad Request
# NONE of the devices are created because one failed validation
# {
#   "2": {"name": ["This field may not be blank."]}
# }
```

## Signals and Webhooks

Despite being called "bulk" operations, NetBox processes each object **sequentially** rather than using Django's `bulk_create()`. This is intentional for validation purposes.

**Key implications:**

- **Signals fire per-object:** Django's `post_save` and `post_delete` signals trigger for each object in the array
- **Webhooks fire per-object:** Each created/updated/deleted object generates its own webhook event
- **Change logging:** Each object gets its own ObjectChange record

This means bulk operations:
- Are **slower** than true database-level bulk inserts
- Are **safer** because all validation and signals execute properly
- Generate **N webhook calls** for N objects (not one aggregated call)

> **Note:** If the same object is modified multiple times within a single request, only the final state triggers the webhook (event deduplication).

## pynetbox Example

```python
import pynetbox

nb = pynetbox.api("https://netbox.example.com", token=TOKEN)

# Bulk create - pass list instead of dict
devices = [
    {"name": f"switch-{i:02d}", "device_type": 1, "role": 1, "site": 1}
    for i in range(10)
]
created = nb.dcim.devices.create(devices)

# Result is list of created Record objects
for device in created:
    print(f"{device.name}: {device.id}")
```

## Async Example

```python
import httpx
import asyncio

async def bulk_create_devices(devices, api_url, headers):
    """Create devices in bulk using async client."""
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{api_url}/dcim/devices/",
            headers=headers,
            json=devices,
            timeout=60  # Bulk operations may take longer
        )
        response.raise_for_status()
        return response.json()
```

## Chunking Large Bulk Operations

For very large bulk operations, consider chunking:

```python
def bulk_create_chunked(endpoint_url, items, headers, chunk_size=100):
    """Create items in chunks to avoid timeouts."""
    created = []

    for i in range(0, len(items), chunk_size):
        chunk = items[i:i + chunk_size]
        response = requests.post(endpoint_url, headers=headers, json=chunk)
        response.raise_for_status()
        created.extend(response.json())

    return created
```

## Exceptions

- **Single object operations:** Use single object JSON for individual creates/updates
- **Very large batches:** May need chunking to avoid timeouts (not atomic across chunks)

## Related Rules

- [rest-patch-vs-put](./rest-patch-vs-put.md) - Use PATCH for partial updates
- [rest-error-handling](./rest-error-handling.md) - Handle bulk operation errors
- [data-dependency-order](./data-dependency-order.md) - Create objects in order

## References

- [NetBox REST API - Creating Objects](https://netboxlabs.com/docs/netbox/en/stable/integrations/rest-api/#creating-objects)
- [NetBox REST API - Updating Objects](https://netboxlabs.com/docs/netbox/en/stable/integrations/rest-api/#updating-objects)
- [NetBox REST API - Deleting Objects](https://netboxlabs.com/docs/netbox/en/stable/integrations/rest-api/#deleting-objects)

```

### references/rules/rest-pagination-required.md

```markdown
---
title: Always Paginate List Requests
impact: HIGH
category: rest
tags: [rest, pagination, performance, lists]
netbox_version: "4.4+"
---

# rest-pagination-required: Always Paginate List Requests

## Rationale

NetBox list endpoints can return thousands of objects. Without pagination:
- Memory exhaustion on client and server
- Request timeouts
- Slow response serialization
- Poor user experience

NetBox defaults to 50 items per page with a maximum of 1000. Always specify pagination explicitly for predictable behavior.

## Incorrect Pattern

```python
# WRONG: No pagination specified
import requests

response = requests.get(
    f"{API_URL}/dcim/devices/",
    headers=headers
)
devices = response.json()["results"]
# Only gets first 50 devices (default limit)
```

```python
# WRONG: Assuming all results are returned
response = requests.get(f"{API_URL}/dcim/devices/", headers=headers)
all_devices = response.json()["results"]  # May be incomplete!
```

**Problems with this approach:**
- Relies on server defaults (may change)
- Gets incomplete data (only first page)
- No explicit control over page size

## Correct Pattern

```python
# CORRECT: Explicit pagination
import requests

def get_all_objects(api_url, endpoint, headers, limit=100):
    """Fetch all objects with proper pagination."""
    all_results = []
    url = f"{api_url}/{endpoint}/?limit={limit}"

    while url:
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        data = response.json()

        all_results.extend(data["results"])
        url = data.get("next")  # None when no more pages

    return all_results

# Usage
all_devices = get_all_objects(API_URL, "dcim/devices", headers)
```

**Benefits:**
- Complete data retrieval
- Explicit page size control
- Memory-efficient processing
- Handles any dataset size

## Response Format

```json
{
    "count": 1500,
    "next": "https://netbox.example.com/api/dcim/devices/?limit=100&offset=100",
    "previous": null,
    "results": [...]
}
```

- `count`: Total objects matching query
- `next`: URL for next page (null if last page)
- `previous`: URL for previous page (null if first page)
- `results`: Array of objects for current page

## Pagination Parameters

| Parameter | Description | Default | Maximum |
|-----------|-------------|---------|---------|
| `limit` | Items per page | 50 | 1000 |
| `offset` | Skip N items | 0 | N/A |

```python
# First page
response = requests.get(f"{API_URL}/dcim/devices/?limit=100&offset=0")

# Second page
response = requests.get(f"{API_URL}/dcim/devices/?limit=100&offset=100")
```

## pynetbox Example

```python
import pynetbox

nb = pynetbox.api("https://netbox.example.com", token=TOKEN)

# pynetbox handles pagination automatically
# .all() returns an iterator that fetches pages as needed
all_devices = nb.dcim.devices.all()

# Process without loading all into memory
for device in all_devices:
    process_device(device)

# Or convert to list if you need random access
device_list = list(nb.dcim.devices.all())
```

## Async Parallel Pagination

```python
import asyncio
import httpx

async def get_all_objects_parallel(api_url, endpoint, headers, limit=100):
    """Fetch all pages concurrently for faster retrieval."""
    async with httpx.AsyncClient(headers=headers) as client:
        # Get total count first
        response = await client.get(f"{api_url}/{endpoint}/?limit=1")
        total = response.json()["count"]

        # Calculate pages
        pages = (total + limit - 1) // limit

        # Fetch all pages concurrently
        tasks = [
            client.get(f"{api_url}/{endpoint}/?limit={limit}&offset={i * limit}")
            for i in range(pages)
        ]
        responses = await asyncio.gather(*tasks)

        # Combine results
        all_results = []
        for resp in responses:
            all_results.extend(resp.json()["results"])

        return all_results
```

## Recommended Page Sizes

| Use Case | Limit |
|----------|-------|
| Interactive UI | 25-50 |
| Background processing | 100-250 |
| Bulk export | 500-1000 |
| Memory-constrained | 50-100 |

## Exceptions

- **Single object:** `GET /api/dcim/devices/123/` doesn't need pagination
- **Count only:** If you only need the count, use `?limit=1` and read `count`

## Related Rules

- [rest-brief-mode](./rest-brief-mode.md) - Reduce payload size
- [graphql-always-paginate](./graphql-always-paginate.md) - GraphQL pagination
- [perf-pagination-strategy](./perf-pagination-strategy.md) - Choosing page sizes

## References

- [NetBox REST API - Pagination](https://netboxlabs.com/docs/netbox/en/stable/integrations/rest-api/#pagination)

```

### references/rules/rest-patch-vs-put.md

```markdown
---
title: Use PATCH for Partial Updates
impact: HIGH
category: rest
tags: [rest, update, patch, put]
netbox_version: "4.4+"
---

# rest-patch-vs-put: Use PATCH for Partial Updates

## Rationale

`PUT` replaces the entire object; omitted fields may be reset to defaults or cleared. `PATCH` updates only the specified fields, leaving others unchanged.

Using `PUT` when you intend `PATCH` can cause data loss or unexpected field resets.

## Incorrect Pattern

```python
# WRONG: Using PUT for partial update
import requests

# Original device has many fields: name, status, comments, tenant, rack, etc.
# We only want to change status

response = requests.put(
    f"{API_URL}/dcim/devices/123/",
    headers=headers,
    json={"status": "active"}  # All other fields omitted!
)
# DANGER: Other fields may be cleared or reset to defaults
```

**Problems with this approach:**
- Omitted required fields cause validation errors
- Omitted optional fields may be cleared
- Must send complete object for PUT
- Easy to accidentally lose data

## Correct Pattern

```python
# CORRECT: Using PATCH for partial update
import requests

API_URL = "https://netbox.example.com/api"
headers = {
    "Authorization": "Bearer nbt_abc123.xxxxx",
    "Content-Type": "application/json"
}

# Only update the fields you want to change
response = requests.patch(
    f"{API_URL}/dcim/devices/123/",
    headers=headers,
    json={"status": "active"}  # Only status changes
)

if response.status_code == 200:
    updated_device = response.json()
    # All other fields are preserved
```

**Benefits:**
- Only specified fields are modified
- Other fields remain unchanged
- No risk of data loss
- Simpler request bodies

## When to Use PUT

Use `PUT` only when you intentionally want to replace the entire object:

```python
# CORRECT use of PUT: Replacing entire object
complete_device = {
    "name": "switch-01",
    "device_type": 1,
    "role": 1,
    "site": 1,
    "status": "active",
    "rack": 5,
    "position": 10,
    "comments": "Replaced device"
}

response = requests.put(
    f"{API_URL}/dcim/devices/123/",
    headers=headers,
    json=complete_device  # Complete object
)
```

## Method Comparison

| Aspect | PATCH | PUT |
|--------|-------|-----|
| Fields sent | Only changed | All fields |
| Omitted fields | Preserved | May be cleared |
| Use case | Partial update | Full replacement |
| Risk | Low | Data loss if incomplete |
| Payload size | Smaller | Larger |

## pynetbox Example

```python
import pynetbox

nb = pynetbox.api("https://netbox.example.com", token=TOKEN)

# pynetbox uses PATCH by default
device = nb.dcim.devices.get(123)
device.status = "active"
device.save()  # PATCH request with only changed fields
```

## Updating Multiple Fields

```python
# PATCH supports updating multiple fields
response = requests.patch(
    f"{API_URL}/dcim/devices/123/",
    headers=headers,
    json={
        "status": "active",
        "comments": "Enabled in maintenance window",
        "custom_fields": {
            "last_maintenance": "2024-01-15"
        }
    }
)
```

## Bulk Updates with PATCH

```python
# Bulk PATCH to list endpoint
updates = [
    {"id": 1, "status": "active"},
    {"id": 2, "status": "active"},
    {"id": 3, "status": "planned"}
]

response = requests.patch(
    f"{API_URL}/dcim/devices/",  # List endpoint
    headers=headers,
    json=updates
)
```

## Exceptions

- **Full object sync:** When syncing from an external system that provides complete objects
- **Object reset:** When intentionally clearing optional fields

## Related Rules

- [rest-list-endpoint-bulk-ops](./rest-list-endpoint-bulk-ops.md) - Bulk update patterns
- [rest-error-handling](./rest-error-handling.md) - Handle update errors
- [rest-idempotency](./rest-idempotency.md) - Idempotent operations

## References

- [RFC 5789 - PATCH Method for HTTP](https://datatracker.ietf.org/doc/html/rfc5789)
- [NetBox REST API - Updating Objects](https://netboxlabs.com/docs/netbox/en/stable/integrations/rest-api/#updating-objects)

```

### references/rules/rest-brief-mode.md

```markdown
---
title: Use Brief Mode for List Operations
impact: HIGH
category: rest
tags: [rest, performance, brief, optimization]
netbox_version: "4.4+"
---

# rest-brief-mode: Use Brief Mode for List Operations

## Rationale

Brief mode (`?brief=True`) returns a minimal representation of objects, typically reducing response size by 90% or more. This dramatically improves:
- Network transfer time
- JSON parsing time
- Memory usage
- Overall response latency

Use brief mode whenever you don't need full object details.

## Incorrect Pattern

```python
# WRONG: Full response when only ID and name needed
import requests

# Fetching device list for a dropdown menu
response = requests.get(
    f"{API_URL}/dcim/devices/",
    headers=headers
)
# Response: ~2KB per device with all fields
# For 1000 devices: ~2MB transfer

# But we only use:
options = [{"value": d["id"], "label": d["name"]} for d in response.json()["results"]]
```

**Problems with this approach:**
- Transferring unnecessary data
- Slower response times
- Higher memory usage
- Wasted bandwidth and processing

## Correct Pattern

```python
# CORRECT: Brief mode for dropdown population
import requests

API_URL = "https://netbox.example.com/api"
headers = {
    "Authorization": "Bearer nbt_abc123.xxxxx",
    "Content-Type": "application/json"
}

response = requests.get(
    f"{API_URL}/dcim/devices/?brief=True",
    headers=headers
)
# Response: ~200 bytes per device
# For 1000 devices: ~200KB transfer (10x smaller)

devices = response.json()["results"]
options = [{"value": d["id"], "label": d["display"]} for d in devices]
```

**Benefits:**
- ~90% reduction in response size
- Faster response times
- Lower memory footprint
- Better scalability

## Brief Response Fields

Brief mode returns only essential fields:

```json
{
    "id": 123,
    "url": "https://netbox.example.com/api/dcim/devices/123/",
    "display": "switch-01",
    "name": "switch-01"
}
```

The exact fields vary by object type but typically include:
- `id`: Object identifier
- `url`: API URL for full object
- `display`: Human-readable display name
- Natural key fields (e.g., `name`, `slug`)

## Size Comparison

| Object Type | Full Response | Brief Response | Reduction |
|-------------|--------------|----------------|-----------|
| Device | ~2,000 bytes | ~200 bytes | 90% |
| Prefix | ~800 bytes | ~150 bytes | 81% |
| Site | ~1,200 bytes | ~180 bytes | 85% |
| Interface | ~600 bytes | ~120 bytes | 80% |

## Use Cases for Brief Mode

| Scenario | Use Brief? |
|----------|-----------|
| Dropdown/select menus | Yes |
| Autocomplete suggestions | Yes |
| Reference lists (for FK selection) | Yes |
| Existence checks | Yes |
| Relationship validation | Yes |
| Displaying object details | No |
| Bulk updates (need current values) | No |
| Reports needing all fields | No |

## Combining with Other Parameters

```python
# Brief mode with filters and pagination
response = requests.get(
    f"{API_URL}/dcim/devices/?brief=True&site=nyc-dc1&limit=100",
    headers=headers
)

# Brief mode with ordering
response = requests.get(
    f"{API_URL}/dcim/devices/?brief=True&ordering=name",
    headers=headers
)
```

## pynetbox Note

pynetbox doesn't have a direct brief mode parameter, but you can use the underlying request:

```python
import pynetbox

nb = pynetbox.api("https://netbox.example.com", token=TOKEN)

# pynetbox always fetches full objects
# For brief mode, use requests directly or filter early

# Alternative: Use filter to limit what you fetch
sites = nb.dcim.sites.filter(status="active")
options = [{"value": s.id, "label": s.name} for s in sites]
```

## Exceptions

- **Detail views:** Need full object data
- **Editing forms:** Need all current field values
- **Nested serializer access:** Brief mode doesn't include nested objects

## Related Rules

- [rest-field-selection](./rest-field-selection.md) - More granular field control
- [rest-exclude-config-context](./rest-exclude-config-context.md) - Exclude heavy fields
- [perf-brief-mode-lists](./perf-brief-mode-lists.md) - Performance impact

## References

- [NetBox REST API - Brief Mode](https://netboxlabs.com/docs/netbox/en/stable/integrations/rest-api/#brief-format)

```

### references/rules/rest-field-selection.md

```markdown
---
title: Use Field Selection for Specific Fields
impact: HIGH
category: rest
tags: [rest, performance, fields, optimization]
netbox_version: "4.4+"
---

# rest-field-selection: Use Field Selection for Specific Fields

## Rationale

The `?fields=` parameter allows selecting exactly which fields to include in the response. This provides more granular control than brief mode when you need specific fields that brief mode doesn't include.

Benefits:
- Reduce payload to only needed data
- Include specific nested fields
- Better than brief when you need certain non-brief fields

## Incorrect Pattern

```python
# WRONG: Fetching full object for just a few fields
import requests

response = requests.get(
    f"{API_URL}/dcim/devices/",
    headers=headers
)

# Full response with 50+ fields, but we only use 4:
for device in response.json()["results"]:
    print(f"{device['name']}: {device['status']} at {device['site']['name']}")
    if device["primary_ip4"]:
        print(f"  IP: {device['primary_ip4']['address']}")
```

**Problems with this approach:**
- Transferring 50+ fields when only 4 needed
- Larger response size
- More data to parse
- Wasted resources

## Correct Pattern

```python
# CORRECT: Request only needed fields
import requests

API_URL = "https://netbox.example.com/api"
headers = {
    "Authorization": "Bearer nbt_abc123.xxxxx",
    "Content-Type": "application/json"
}

response = requests.get(
    f"{API_URL}/dcim/devices/?fields=id,name,status,site,primary_ip4",
    headers=headers
)

for device in response.json()["results"]:
    print(f"{device['name']}: {device['status']} at {device['site']['name']}")
    if device.get("primary_ip4"):
        print(f"  IP: {device['primary_ip4']['address']}")
```

**Benefits:**
- Response contains exactly what's needed
- Smaller payload
- Faster processing
- Clear intent in code

## Field Selection Syntax

```
?fields=field1,field2,field3
```

**Selecting nested fields:**
```
?fields=name,site.name,device_type.model
```

**Multiple fields from same nested object:**
```
?fields=name,site.name,site.slug,device_type.model,device_type.manufacturer.name
```

## Response Examples

**Full response (no field selection):**
```json
{
    "id": 123,
    "name": "switch-01",
    "status": {"value": "active", "label": "Active"},
    "site": {"id": 1, "url": "...", "display": "NYC-DC1", "name": "NYC-DC1", "slug": "nyc-dc1"},
    "device_type": {"id": 1, "url": "...", ...},
    "role": {"id": 1, "url": "...", ...},
    "tenant": null,
    "platform": null,
    ... // 40+ more fields
}
```

**With field selection (`?fields=id,name,status,site.name`):**
```json
{
    "id": 123,
    "name": "switch-01",
    "status": {"value": "active", "label": "Active"},
    "site": {"name": "NYC-DC1"}
}
```

## Common Field Selection Patterns

**Device inventory list:**
```python
fields = "id,name,status,site.name,rack.name,primary_ip4.address"
response = requests.get(f"{API_URL}/dcim/devices/?fields={fields}", headers=headers)
```

**Interface summary:**
```python
fields = "id,name,type,enabled,device.name"
response = requests.get(f"{API_URL}/dcim/interfaces/?fields={fields}", headers=headers)
```

**Prefix allocation status:**
```python
fields = "id,prefix,status,site.name,vlan.vid,vlan.name"
response = requests.get(f"{API_URL}/ipam/prefixes/?fields={fields}", headers=headers)
```

## Field Selection vs Brief Mode

| Feature | `?fields=` | `?brief=True` |
|---------|-----------|---------------|
| Control | Choose any fields | Fixed minimal set |
| Nested fields | Yes | No |
| Flexibility | High | Low |
| Use case | Specific field needs | Dropdowns/references |

**Use brief mode when:**
- Building dropdowns or select lists
- Only need ID and display name

**Use field selection when:**
- Need specific non-brief fields
- Need nested object fields
- Need custom field combinations

## Combining with Other Parameters

```python
# Field selection with filters and pagination
response = requests.get(
    f"{API_URL}/dcim/devices/"
    f"?fields=id,name,status,site.name"
    f"&status=active"
    f"&site=nyc-dc1"
    f"&limit=100",
    headers=headers
)
```

## Exceptions

- **Unknown field requirements:** May need full object for dynamic processing
- **Cache reuse:** Full objects may be more cache-efficient if reused
- **Audit/logging:** May want complete snapshots

## Related Rules

- [rest-brief-mode](./rest-brief-mode.md) - Simpler minimal response
- [rest-exclude-config-context](./rest-exclude-config-context.md) - Exclude heavy fields
- [graphql-select-only-needed](./graphql-select-only-needed.md) - GraphQL field selection

## References

- [NetBox REST API - Field Selection](https://netboxlabs.com/docs/netbox/en/stable/integrations/rest-api/)

```

### references/rules/rest-exclude-config-context.md

```markdown
---
title: Exclude Config Context from Device Lists
impact: HIGH
category: rest
tags: [rest, performance, config-context, optimization]
netbox_version: "4.4+"
---

# rest-exclude-config-context: Exclude Config Context from Device Lists

## Rationale

Config context is the single most expensive field to compute for device queries. It requires:
- Traversing the device hierarchy (region → site → location → rack → device)
- Evaluating config context rules at each level
- Merging multiple context sources with precedence
- Serializing potentially large JSON structures

**Community-reported impact:** Device list requests can be **10-100x slower** with config context included. This is confirmed across multiple large NetBox deployments.

Always exclude config context from list operations unless specifically needed.

## Incorrect Pattern

```python
# WRONG: Fetching device list with config context (implicit)
import requests

response = requests.get(
    f"{API_URL}/dcim/devices/",
    headers=headers
)
# Response includes config_context for each device
# With 1000 devices: may take 30+ seconds instead of <1 second
```

**Problems with this approach:**
- Config context computed for every device
- Dramatically slower response times
- Often the config context isn't even used
- May cause request timeouts with large datasets

## Correct Pattern

```python
# CORRECT: Exclude config context from device lists
import requests

API_URL = "https://netbox.example.com/api"
headers = {
    "Authorization": "Bearer nbt_abc123.xxxxx",
    "Content-Type": "application/json"
}

response = requests.get(
    f"{API_URL}/dcim/devices/?exclude=config_context",
    headers=headers
)
# Response is 10-100x faster
```

**Benefits:**
- Dramatically faster response times
- Reduced database load
- Better scalability
- No unnecessary computation

## Performance Comparison

| Scenario | With config_context | Without (excluded) |
|----------|--------------------|--------------------|
| 100 devices | 2-5 seconds | 0.1-0.2 seconds |
| 1000 devices | 20-60 seconds | 0.5-1 second |
| 5000 devices | Timeout likely | 2-5 seconds |

Actual times vary based on config context complexity and server resources.

## When Config Context IS Needed

Fetch config context only when specifically required:

```python
# For individual device where config context is needed
response = requests.get(
    f"{API_URL}/dcim/devices/123/",  # Single device, full details
    headers=headers
)
config = response.json()["config_context"]

# Or fetch for specific devices only
device_ids = [1, 2, 3]
for device_id in device_ids:
    response = requests.get(
        f"{API_URL}/dcim/devices/{device_id}/",
        headers=headers
    )
    # Process config context individually
```

## Combining with Other Optimizations

```python
# Maximum optimization for device lists
response = requests.get(
    f"{API_URL}/dcim/devices/"
    f"?exclude=config_context"  # Exclude heavy computation
    f"&brief=True"              # Minimal fields
    f"&limit=100",              # Paginate
    headers=headers
)
```

## Excluding Multiple Fields

The `exclude` parameter accepts comma-separated field names:

```python
# Exclude multiple heavy fields
response = requests.get(
    f"{API_URL}/dcim/devices/?exclude=config_context,local_context_data",
    headers=headers
)
```

## pynetbox Consideration

pynetbox doesn't directly support the `exclude` parameter. For performance-critical list operations, use requests directly:

```python
import requests
import pynetbox

# For performance-critical lists, use requests
response = requests.get(
    f"{API_URL}/dcim/devices/?exclude=config_context&limit=1000",
    headers=headers
)
devices = response.json()["results"]

# pynetbox for individual device operations
nb = pynetbox.api("https://netbox.example.com", token=TOKEN)
device = nb.dcim.devices.get(123)  # Single device is fine
config = device.config_context
```

## Virtual Machines

The same applies to virtual machines:

```python
response = requests.get(
    f"{API_URL}/virtualization/virtual-machines/?exclude=config_context",
    headers=headers
)
```

## Exceptions

- **Single device/VM fetch:** Config context overhead is acceptable for individual objects
- **Config rendering:** When you specifically need to render device configuration
- **Automation requiring context:** Ansible, Nornir workflows that use config context

## Related Rules

- [rest-brief-mode](./rest-brief-mode.md) - Use brief mode for lists
- [rest-field-selection](./rest-field-selection.md) - Select only needed fields
- [perf-exclude-config-context](./perf-exclude-config-context.md) - Performance impact details
- [rest-avoid-search-filter-at-scale](./rest-avoid-search-filter-at-scale.md) - Another major performance issue

## References

- [NetBox GitHub Discussion - Performance Issues](https://github.com/netbox-community/netbox/discussions)
- [NetBox Config Contexts](https://netboxlabs.com/docs/netbox/en/stable/features/context-data/)

```

### references/rules/rest-avoid-search-filter-at-scale.md

```markdown
---
title: Avoid Search Filter at Scale
impact: HIGH
category: rest
tags: [rest, performance, search, filtering]
netbox_version: "4.4+"
---

# rest-avoid-search-filter-at-scale: Avoid Search Filter at Scale

## Rationale

The `q=` search parameter provides simple text search across multiple fields, but it becomes extremely slow with large datasets. This is especially severe for devices with primary IPs assigned.

Community reports indicate that `q=` queries can take orders of magnitude longer than equivalent specific filters on deployments with thousands of devices.

## Incorrect Pattern

```python
# WRONG: Using generic search with large datasets
import requests

# This can take 10-60+ seconds with thousands of devices
response = requests.get(
    f"{API_URL}/dcim/devices/?q=switch",
    headers=headers
)
```

**Problems with this approach:**
- `q=` searches across multiple fields simultaneously
- No index optimization for this search pattern
- Dramatically slower as dataset grows
- Especially slow when devices have primary IPs assigned

## Correct Pattern

```python
# CORRECT: Use specific filters instead of generic search
import requests

API_URL = "https://netbox.example.com/api"
headers = {
    "Authorization": "Bearer nbt_abc123.xxxxx",
    "Content-Type": "application/json"
}

# Search by name (indexed, fast)
response = requests.get(
    f"{API_URL}/dcim/devices/?name__ic=switch",
    headers=headers
)

# Or by multiple specific fields
response = requests.get(
    f"{API_URL}/dcim/devices/?name__ic=switch&status=active",
    headers=headers
)
```

**Benefits:**
- Uses indexed database queries
- Orders of magnitude faster
- Predictable performance at scale

## Performance Comparison

| Query | 100 devices | 5000 devices | 20000 devices |
|-------|-------------|--------------|---------------|
| `q=switch` | 0.5s | 10-30s | 60s+ / timeout |
| `name__ic=switch` | 0.1s | 0.3s | 0.8s |

Times are approximate and vary by hardware and data characteristics.

## Specific Filter Alternatives

| Instead of `q=` for... | Use specific filter |
|------------------------|---------------------|
| Device name | `name__ic=` |
| Serial number | `serial__ic=` |
| Asset tag | `asset_tag__ic=` |
| Comments | `comments__ic=` |
| Site name | `site=` (exact) or filter site first |

## Multi-Field Search Alternative

If you need to search across multiple fields, do it in parallel with specific filters:

```python
import asyncio
import httpx

async def search_devices(term, api_url, headers):
    """Search devices across multiple fields with specific filters."""
    async with httpx.AsyncClient(headers=headers) as client:
        # Search different fields in parallel
        tasks = [
            client.get(f"{api_url}/dcim/devices/?name__ic={term}&limit=50"),
            client.get(f"{api_url}/dcim/devices/?serial__ic={term}&limit=50"),
            client.get(f"{api_url}/dcim/devices/?asset_tag__ic={term}&limit=50"),
        ]

        responses = await asyncio.gather(*tasks)

        # Combine and deduplicate results
        seen_ids = set()
        results = []
        for resp in responses:
            for device in resp.json()["results"]:
                if device["id"] not in seen_ids:
                    seen_ids.add(device["id"])
                    results.append(device)

        return results
```

## Client-Side Search

For small datasets or autocomplete, consider fetching filtered data and searching client-side:

```python
# For autocomplete with <1000 items, this may be faster overall
# Fetch once, cache, and filter client-side

devices = requests.get(
    f"{API_URL}/dcim/devices/?brief=True&status=active",
    headers=headers
).json()["results"]

# Client-side filtering (instant)
def search_devices(term, devices):
    term_lower = term.lower()
    return [d for d in devices if term_lower in d["name"].lower()]
```

## When `q=` is Acceptable

- **Small datasets:** < 500 total objects
- **One-off queries:** Manual exploration, not production code
- **Admin/debugging:** When you accept the performance cost

## Exceptions

- **Small NetBox instances:** With < 1000 devices, `q=` may be acceptable
- **Non-device endpoints:** Some object types may not have this issue

## Related Rules

- [rest-filtering-expressions](./rest-filtering-expressions.md) - Use lookup expressions
- [rest-exclude-config-context](./rest-exclude-config-context.md) - Another major performance issue
- [perf-parallel-requests](./perf-parallel-requests.md) - Parallelize searches

## References

- [NetBox GitHub Discussions - Search Performance](https://github.com/netbox-community/netbox/discussions)
- [NetBox Filtering](https://netboxlabs.com/docs/netbox/en/stable/integrations/rest-api/#filtering)

```

### references/rules/rest-filtering-expressions.md

```markdown
---
title: Use Lookup Expressions for Efficient Filtering
impact: MEDIUM
category: rest
tags: [rest, filtering, expressions, queries]
netbox_version: "4.4+"
---

# rest-filtering-expressions: Use Lookup Expressions for Efficient Filtering

## Rationale

NetBox provides powerful filtering with lookup expressions beyond simple equality. Using these expressions:
- Reduces data transfer (filter server-side)
- Leverages database indexes
- Enables complex queries in single requests
- Avoids inefficient client-side filtering

## Incorrect Pattern

```python
# WRONG: Fetching all, filtering client-side
import requests

response = requests.get(f"{API_URL}/dcim/devices/", headers=headers)
devices = response.json()["results"]

# Client-side filtering (inefficient)
active_switches = [
    d for d in devices
    if d["status"]["value"] == "active"
    and "switch" in d["name"].lower()
]
```

**Problems with this approach:**
- Downloads all devices (potentially thousands)
- Filtering happens after full transfer
- Wastes bandwidth and memory
- Slower overall

## Correct Pattern

```python
# CORRECT: Server-side filtering with expressions
import requests

API_URL = "https://netbox.example.com/api"
headers = {
    "Authorization": "Bearer nbt_abc123.xxxxx",
    "Content-Type": "application/json"
}

# Filter on server, get only matching results
response = requests.get(
    f"{API_URL}/dcim/devices/?status=active&name__ic=switch",
    headers=headers
)
devices = response.json()["results"]
```

**Benefits:**
- Only matching objects transferred
- Database-optimized queries
- Lower bandwidth usage
- Faster overall response

## Available Lookup Expressions

### String Expressions

| Expression | Description | Example |
|------------|-------------|---------|
| (none) | Exact match | `name=switch-01` |
| `__n` | Not equal | `name__n=test-device` |
| `__ic` | Contains (case-insensitive) | `name__ic=switch` |
| `__nic` | Not contains | `name__nic=test` |
| `__isw` | Starts with | `name__isw=core-` |
| `__nisw` | Not starts with | `name__nisw=temp-` |
| `__iew` | Ends with | `name__iew=-prod` |
| `__niew` | Not ends with | `name__niew=-dev` |
| `__ie` | Exact (case-insensitive) | `name__ie=Switch-01` |
| `__nie` | Not exact (case-insensitive) | `name__nie=Router-01` |
| `__empty` | Is empty | `comments__empty=true` |

### Numeric Expressions

| Expression | Description | Example |
|------------|-------------|---------|
| `__gte` | Greater than or equal | `vlan_id__gte=100` |
| `__gt` | Greater than | `vlan_id__gt=99` |
| `__lte` | Less than or equal | `vlan_id__lte=200` |
| `__lt` | Less than | `vlan_id__lt=201` |

### Null Expressions

| Expression | Description | Example |
|------------|-------------|---------|
| `__isnull` | Is null | `primary_ip4__isnull=false` |

## Common Use Cases

### Find devices by name pattern

```python
# Devices with "core" in the name
response = requests.get(
    f"{API_URL}/dcim/devices/?name__ic=core",
    headers=headers
)

# Devices starting with "sw-"
response = requests.get(
    f"{API_URL}/dcim/devices/?name__isw=sw-",
    headers=headers
)
```

### VLAN range queries

```python
# VLANs between 100 and 200
response = requests.get(
    f"{API_URL}/ipam/vlans/?vid__gte=100&vid__lte=200",
    headers=headers
)
```

### Devices with IP addresses assigned

```python
# Devices that have a primary IPv4
response = requests.get(
    f"{API_URL}/dcim/devices/?primary_ip4__isnull=false",
    headers=headers
)

# Devices without primary IPv4
response = requests.get(
    f"{API_URL}/dcim/devices/?primary_ip4__isnull=true",
    headers=headers
)
```

### Exclude certain values

```python
# All devices NOT in "offline" status
response = requests.get(
    f"{API_URL}/dcim/devices/?status__n=offline",
    headers=headers
)

# Devices not at test sites
response = requests.get(
    f"{API_URL}/dcim/devices/?site__n=test-site-1&site__n=test-site-2",
    headers=headers
)
```

### Multiple values (OR logic)

```python
# Devices that are active OR planned
response = requests.get(
    f"{API_URL}/dcim/devices/?status=active&status=planned",
    headers=headers
)
```

### Combining filters (AND logic)

```python
# Active devices with "core" in name at specific site
response = requests.get(
    f"{API_URL}/dcim/devices/?status=active&name__ic=core&site=nyc-dc1",
    headers=headers
)
```

### Prefix queries

```python
# All prefixes with /24 or smaller
response = requests.get(
    f"{API_URL}/ipam/prefixes/?prefix_length__gte=24",
    headers=headers
)

# Prefixes within a container
response = requests.get(
    f"{API_URL}/ipam/prefixes/?within=10.0.0.0/8",
    headers=headers
)
```

## pynetbox Examples

```python
import pynetbox

nb = pynetbox.api("https://netbox.example.com", token=TOKEN)

# Filter with lookup expressions
devices = nb.dcim.devices.filter(
    name__ic="switch",
    status="active"
)

# VLAN range
vlans = nb.ipam.vlans.filter(
    vid__gte=100,
    vid__lte=200
)

# Negation
non_offline = nb.dcim.devices.filter(
    status__n="offline"
)
```

## Exceptions

- **Complex logic:** Some queries may require multiple requests or client-side processing
- **Regex patterns:** NetBox doesn't support regex; use multiple filters

## Related Rules

- [rest-custom-field-filters](./rest-custom-field-filters.md) - Filter by custom fields
- [rest-avoid-search-filter-at-scale](./rest-avoid-search-filter-at-scale.md) - Avoid `q=` at scale
- [graphql-prefer-filters](./graphql-prefer-filters.md) - GraphQL filtering

## References

- [NetBox Filtering](https://netboxlabs.com/docs/netbox/en/stable/integrations/rest-api/#filtering)

```

### references/rules/rest-custom-field-filters.md

```markdown
---
title: Filter by Custom Fields
impact: MEDIUM
category: rest
tags: [rest, filtering, custom-fields]
netbox_version: "4.4+"
---

# rest-custom-field-filters: Filter by Custom Fields

## Rationale

Custom fields extend NetBox's data model. Filter by custom fields using the `cf_` prefix to efficiently query organization-specific data.

## Correct Pattern

```python
import requests

# Filter by custom field value
response = requests.get(
    f"{API_URL}/dcim/devices/?cf_environment=production",
    headers=headers
)

# Multiple custom field filters (AND logic)
response = requests.get(
    f"{API_URL}/dcim/devices/?cf_environment=production&cf_tier=1",
    headers=headers
)

# Custom field with lookup expression
response = requests.get(
    f"{API_URL}/dcim/devices/?cf_deployment_date__gte=2024-01-01",
    headers=headers
)
```

## Common Custom Field Types

| Type | Filter Example |
|------|---------------|
| Text | `cf_owner__ic=team` |
| Integer | `cf_priority__gte=3` |
| Boolean | `cf_monitored=true` |
| Date | `cf_expiry__lte=2024-12-31` |
| Selection | `cf_environment=production` |

## Related Rules

- [rest-filtering-expressions](./rest-filtering-expressions.md) - Lookup expressions
- [data-custom-fields](./data-custom-fields.md) - Custom field usage

```

### references/rules/rest-nested-serializers.md

```markdown
---
title: Understand Nested vs Flat Serializers
impact: LOW
category: rest
tags: [rest, serializers, nested, response]
netbox_version: "4.4+"
---

# rest-nested-serializers: Understand Nested vs Flat Serializers

## Rationale

NetBox responses include nested object representations for related objects. Understanding this structure helps parse responses and construct requests correctly.

## Response Structure

```json
{
  "id": 123,
  "name": "switch-01",
  "site": {
    "id": 1,
    "url": "https://netbox.example.com/api/dcim/sites/1/",
    "display": "NYC-DC1",
    "name": "NYC-DC1",
    "slug": "nyc-dc1"
  },
  "device_type": {
    "id": 1,
    "url": "...",
    "display": "Catalyst 9300",
    "manufacturer": {
      "id": 1,
      "url": "...",
      "display": "Cisco",
      "name": "Cisco",
      "slug": "cisco"
    },
    "model": "Catalyst 9300"
  }
}
```

## Request Structure

When creating/updating, use integer IDs:

```python
# CORRECT: Use IDs for foreign keys
device_data = {
    "name": "switch-01",
    "site": 1,           # Integer ID, not nested object
    "device_type": 1,
    "role": 1
}

response = requests.post(f"{API_URL}/dcim/devices/", headers=headers, json=device_data)
```

## Related Rules

- [rest-brief-mode](./rest-brief-mode.md) - Minimal nested data
- [rest-field-selection](./rest-field-selection.md) - Control nested fields

```

### references/rules/rest-ordering-results.md

```markdown
---
title: Use Ordering Parameter for Sorted Results
impact: LOW
category: rest
tags: [rest, ordering, sorting, queries]
netbox_version: "4.4+"
---

# rest-ordering-results: Use Ordering Parameter for Sorted Results

## Rationale

Use the `?ordering=` parameter to get sorted results from the server rather than sorting client-side. This is more efficient and consistent.

## Correct Pattern

```python
import requests

# Ascending order by name
response = requests.get(
    f"{API_URL}/dcim/devices/?ordering=name",
    headers=headers
)

# Descending order (prefix with -)
response = requests.get(
    f"{API_URL}/dcim/devices/?ordering=-created",
    headers=headers
)

# Multiple fields (comma-separated)
response = requests.get(
    f"{API_URL}/dcim/devices/?ordering=site,name",
    headers=headers
)
```

## Common Ordering Fields

- `name` - Alphabetical by name
- `created` - By creation date
- `-created` - Newest first
- `last_updated` - By modification date
- `id` - By database ID

## Related Rules

- [rest-pagination-required](./rest-pagination-required.md) - Pagination
- [rest-filtering-expressions](./rest-filtering-expressions.md) - Filtering

```

### references/rules/rest-options-discovery.md

```markdown
---
title: Use OPTIONS for Endpoint Discovery
impact: LOW
category: rest
tags: [rest, discovery, options, schema]
netbox_version: "4.4+"
---

# rest-options-discovery: Use OPTIONS for Endpoint Discovery

## Rationale

The HTTP OPTIONS method returns endpoint schema including available fields, types, and choices. This is useful for dynamic integrations and validation.

## Correct Pattern

```python
import requests

response = requests.options(f"{API_URL}/dcim/devices/", headers=headers)
schema = response.json()

# Discover available fields
for field, meta in schema["actions"]["POST"].items():
    field_type = meta.get("type", "unknown")
    required = meta.get("required", False)
    choices = meta.get("choices", [])
    print(f"{field}: {field_type}, required={required}")
    if choices:
        print(f"  Choices: {[c['value'] for c in choices]}")
```

## Use Cases

- Dynamic form generation
- Runtime field validation
- API documentation tools
- Choice field discovery

## Related Rules

- [rest-error-handling](./rest-error-handling.md) - Handle API responses

```

### references/rules/graphql-use-query-optimizer.md

```markdown
---
title: Use Query Optimizer for All GraphQL Queries
impact: CRITICAL
category: graphql
tags: [graphql, performance, optimization, tooling]
netbox_version: "4.4+"
---

# graphql-use-query-optimizer: Use Query Optimizer for All GraphQL Queries

## Rationale

The [netbox-graphql-query-optimizer](https://github.com/netboxlabs/netbox-graphql-query-optimizer) is an essential tool for production GraphQL usage. It performs static analysis to detect query patterns that cause severe performance problems.

**Real-world impact:** Query complexity scores have been reduced from 20,500 to 17 (~1,200x improvement) using this tool.

Without query analysis, it's easy to write queries that:
- Trigger N+1 database queries
- Fetch unbounded result sets
- Create multiplicative fan-out patterns
- Exceed safe query depth

## Incorrect Pattern

```graphql
# WRONG: Query written without analysis
# This may have N+1 issues, unbounded lists, or fan-out problems
query {
  site_list {
    name
    devices {
      name
      interfaces {
        name
        ip_addresses {
          address
        }
      }
    }
  }
}
```

**Problems with this approach:**
- No pagination limits (could return thousands of objects)
- Nested lists create fan-out (sites × devices × interfaces × IPs)
- Deep nesting (4 levels)
- No visibility into actual complexity

## Correct Pattern

First, install and run the query optimizer:

```bash
pip install netbox-graphql-query-optimizer

# Analyze the query
netbox-query-optimizer analyze query.graphql
```

**Output example:**
```
Query Analysis Report
=====================
Complexity Score: 20,500 (CRITICAL - exceeds threshold of 1,000)

Issues Found:
- UNBOUNDED_LIST: site_list has no pagination
- UNBOUNDED_LIST: devices has no pagination
- UNBOUNDED_LIST: interfaces has no pagination
- UNBOUNDED_LIST: ip_addresses has no pagination
- FAN_OUT: Multiplicative nesting detected
- DEPTH_EXCEEDED: Query depth 4 exceeds recommended max of 3

Recommendations:
1. Add limit parameter to all list queries
2. Reduce nesting depth or split into multiple queries
3. Consider REST API for this use case
```

**Corrected query after analysis:**

```graphql
# CORRECT: Query optimized based on analyzer feedback
query GetSiteSummary {
  site_list(limit: 10) {
    id
    name
    device_count
  }
}

# Separate query for device details when needed
query GetSiteDevices($siteId: Int!) {
  device_list(site_id: $siteId, limit: 50) {
    name
    interface_count
  }
}

# Separate query for interface details when needed
query GetDeviceInterfaces($deviceId: Int!) {
  interface_list(device_id: $deviceId, limit: 100) {
    name
    ip_addresses(limit: 10) {
      address
    }
  }
}
```

**After optimization:**
```
Complexity Score: 17 (OK)
No issues found.
```

## Calibration for Production

Default scores are estimates. Calibrate against your actual data for accurate scoring:

```bash
netbox-query-optimizer analyze query.graphql \
  --calibrate \
  --url https://netbox.example.com \
  --token nbt_abc123.xxxxx
```

This fetches real object counts from your NetBox instance to compute realistic complexity.

## Integration into CI/CD

Add query analysis to your CI pipeline:

```yaml
# .github/workflows/check-queries.yml
name: Validate GraphQL Queries
on: [push, pull_request]

jobs:
  analyze:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install netbox-graphql-query-optimizer
      - run: |
          for query in queries/*.graphql; do
            echo "Analyzing $query..."
            netbox-query-optimizer analyze "$query" --max-score 500
          done
```

## Complexity Budget Guidelines

| Query Type | Max Score | Use Case |
|------------|-----------|----------|
| Dashboard widgets | 50 | Real-time display |
| Detail views | 200 | Single object with relations |
| Reports | 500 | Batch data retrieval |
| ETL/sync | 1000 | Background processing |

## Exceptions

- **Prototype/development:** Quick testing without optimization is acceptable
- **One-off queries:** Manual queries for debugging don't need formal analysis

But any query used in production applications MUST be analyzed.

## Related Rules

- [graphql-always-paginate](./graphql-always-paginate.md) - Every list needs limits
- [graphql-pagination-at-each-level](./graphql-pagination-at-each-level.md) - Paginate nested lists
- [graphql-max-depth](./graphql-max-depth.md) - Keep depth ≤3
- [graphql-complexity-budgets](./graphql-complexity-budgets.md) - Establish budgets

## References

- [netbox-graphql-query-optimizer](https://github.com/netboxlabs/netbox-graphql-query-optimizer)
- [NetBox GraphQL API](https://netboxlabs.com/docs/netbox/en/stable/integrations/graphql-api/)

```

### references/rules/graphql-always-paginate.md

```markdown
---
title: Always Paginate GraphQL List Queries
impact: CRITICAL
category: graphql
tags: [graphql, pagination, performance]
netbox_version: "4.4+"
---

# graphql-always-paginate: Always Paginate GraphQL List Queries

## Rationale

Unbounded GraphQL queries can return thousands or millions of objects, causing:
- Server memory exhaustion
- Request timeouts
- Client application crashes
- Database performance degradation

Every list query MUST include explicit pagination limits.

## Incorrect Pattern

```graphql
# WRONG: No pagination - could return entire database
query {
  device_list {
    name
    status
    site {
      name
    }
  }
}
```

**Problems with this approach:**
- No limit on returned objects
- Could return 10,000+ devices
- Server must serialize entire result set
- Client must process/store all results
- Network transfer of potentially megabytes of data

## Correct Pattern

```graphql
# CORRECT: Explicit pagination with limit and offset
query GetDevices($limit: Int!, $offset: Int!) {
  device_list(limit: $limit, offset: $offset) {
    name
    status
    site {
      name
    }
  }
}
```

**Variables:**
```json
{
  "limit": 100,
  "offset": 0
}
```

**Benefits:**
- Predictable response size
- Controlled memory usage
- Faster query execution
- Enables incremental data loading

## Pagination Patterns

### Basic Pagination

```graphql
query GetDevicesPage($limit: Int!, $offset: Int!) {
  device_list(limit: $limit, offset: $offset) {
    id
    name
    status
  }
}
```

### With Total Count

```graphql
query GetDevicesWithCount($limit: Int!, $offset: Int!) {
  device_list(limit: $limit, offset: $offset) {
    id
    name
  }
  # Get total count for pagination UI
  device_count: device_list {
    id
  }
}
```

Note: Getting count requires a separate query or including minimal fields.

### Full Pagination Implementation (Python)

```python
import requests

def fetch_all_devices_graphql(netbox_url, token, page_size=100):
    """Fetch all devices with proper pagination."""
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    query = """
    query GetDevices($limit: Int!, $offset: Int!) {
      device_list(limit: $limit, offset: $offset) {
        id
        name
        status
        site {
          name
        }
      }
    }
    """

    all_devices = []
    offset = 0

    while True:
        response = requests.post(
            f"{netbox_url}/graphql/",
            headers=headers,
            json={
                "query": query,
                "variables": {"limit": page_size, "offset": offset}
            }
        )
        response.raise_for_status()

        data = response.json()
        if "errors" in data:
            raise Exception(f"GraphQL errors: {data['errors']}")

        devices = data["data"]["device_list"]

        if not devices:
            break

        all_devices.extend(devices)

        if len(devices) < page_size:
            break

        offset += page_size

    return all_devices
```

## Recommended Page Sizes

| Use Case | Page Size |
|----------|-----------|
| Interactive UI | 25-50 |
| Background sync | 100-250 |
| Bulk export | 500 |
| Maximum (not recommended) | 1000 |

## Offset Pagination Limitations (Large Datasets)

NetBox's GraphQL API uses **offset-based pagination** (`limit` + `offset`), which has significant performance implications for large datasets.

**The Problem:** Offset pagination requires the database to scan all rows up to the offset position before returning results. As you paginate deeper into results, queries become progressively slower:

| Page | Offset | Performance Impact |
|------|--------|-------------------|
| 1 | 0 | Fast |
| 10 | 900 | Noticeable delay |
| 100 | 9,900 | Slow |
| 1000 | 99,900 | Very slow / timeout risk |

**Version-Specific Pagination Options:**

| NetBox Version | Pagination Method | Notes |
|----------------|-------------------|-------|
| 4.4.x | Offset only | `limit` + `offset` parameters |
| 4.5.x | Offset + ID range emulation | Filter by ID ranges as workaround |
| 4.6.0+ (planned) | Cursor-based | `start` + `limit` parameters |

### Workaround for 4.5.x: ID Range Filtering

In NetBox 4.5.x, you can emulate cursor-based pagination by filtering on ID ranges:

```graphql
# Instead of offset pagination for deep pages
query GetDevicesPage($minId: Int!, $limit: Int!) {
  device_list(
    limit: $limit
    filters: { id__gte: $minId }
  ) {
    id
    name
    status
  }
}
```

```python
def fetch_all_with_id_cursor(netbox_url, token, page_size=100):
    """Fetch all devices using ID-based cursor pagination (4.5.x)."""
    query = """
    query GetDevices($minId: Int!, $limit: Int!) {
      device_list(limit: $limit, filters: { id__gte: $minId }) {
        id
        name
        status
      }
    }
    """

    all_devices = []
    min_id = 0

    while True:
        response = graphql_request(
            netbox_url, token, query,
            {"minId": min_id, "limit": page_size}
        )
        devices = response["data"]["device_list"]

        if not devices:
            break

        all_devices.extend(devices)

        if len(devices) < page_size:
            break

        # Next page starts after the highest ID we received
        min_id = max(d["id"] for d in devices) + 1

    return all_devices
```

**Drawbacks of ID range emulation:**
- Inconsistent page sizes if IDs have gaps (deleted objects)
- Requires fetching `id` field in every query
- No built-in way to know total count or "last page"

### Future: Cursor-Based Pagination (4.6.0+)

NetBox 4.6.0 plans to introduce proper cursor-based pagination with a `start` parameter:

```graphql
# Planned syntax for 4.6.0+
query GetDevices {
  device_list(start: 1000, limit: 100) {
    id
    name
  }
}
```

This will use efficient primary key filtering (`pk >= start`) instead of offset scanning, providing consistent performance regardless of pagination depth.

See [GitHub Issue #21110](https://github.com/netbox-community/netbox/issues/21110) for implementation details and status.

## Exceptions

- **Single object queries:** `device(id: 123)` doesn't need pagination
- **Count-only queries:** Just counting objects doesn't return full data

But ANY query returning a list MUST be paginated.

## Related Rules

- [graphql-use-query-optimizer](./graphql-use-query-optimizer.md) - Analyzer detects unbounded queries
- [graphql-pagination-at-each-level](./graphql-pagination-at-each-level.md) - Paginate nested lists
- [rest-pagination-required](./rest-pagination-required.md) - REST pagination

## References

- [NetBox GraphQL API](https://netboxlabs.com/docs/netbox/en/stable/integrations/graphql-api/)

```

### references/rules/graphql-pagination-at-each-level.md

```markdown
---
title: Paginate Nested Lists at Every Level
impact: HIGH
category: graphql
tags: [graphql, pagination, nesting, performance]
netbox_version: "4.4+"
---

# graphql-pagination-at-each-level: Paginate Nested Lists at Every Level

## Rationale

In GraphQL, pagination isn't just for the top-level query. Every nested list that returns multiple objects must also be paginated. Without nested pagination:
- Each parent can return unbounded children
- Object counts multiply (fan-out pattern)
- 10 sites × 100 devices × 50 interfaces = 50,000 objects

This multiplicative growth can overwhelm both server and client.

## Incorrect Pattern

```graphql
# WRONG: Only top level paginated, nested lists unbounded
query {
  site_list(limit: 10) {
    name
    devices {  # UNBOUNDED - could be hundreds per site
      name
      interfaces {  # UNBOUNDED - could be hundreds per device
        name
        ip_addresses {  # UNBOUNDED - multiple per interface
          address
        }
      }
    }
  }
}
```

**Problems with this approach:**
- 10 sites with 100 devices each = 1,000 devices
- 1,000 devices with 50 interfaces each = 50,000 interfaces
- 50,000 interfaces with 2 IPs each = 100,000 IP addresses
- Total: 151,010 objects from what looks like a "10 sites" query

## Correct Pattern

```graphql
# CORRECT: Every list has pagination limits
query GetSiteOverview {
  site_list(limit: 10) {
    name
    devices(limit: 20) {  # Limit devices per site
      name
      interfaces(limit: 50) {  # Limit interfaces per device
        name
        ip_addresses(limit: 5) {  # Limit IPs per interface
          address
        }
      }
    }
  }
}
```

**With proper limits:**
- 10 sites × 20 devices × 50 interfaces × 5 IPs = 50,000 max
- Still potentially large, but bounded and predictable

## Better: Use Multiple Targeted Queries

```graphql
# Query 1: Get sites with device counts
query GetSites {
  site_list(limit: 10) {
    id
    name
    device_count
  }
}

# Query 2: Get devices for a specific site
query GetSiteDevices($siteId: Int!) {
  device_list(site_id: $siteId, limit: 50) {
    id
    name
    interface_count
  }
}

# Query 3: Get interfaces for specific devices
query GetDeviceInterfaces($deviceId: Int!) {
  interface_list(device_id: $deviceId, limit: 100) {
    name
    ip_addresses(limit: 10) {
      address
    }
  }
}
```

**Benefits:**
- Each query is bounded
- Fetch only what you need when you need it
- Better for UI (fetch on demand)
- Easier to cache

## Implementing Nested Pagination in Code

```python
import requests

def fetch_site_with_devices(site_id, netbox_url, token, devices_limit=50):
    """Fetch site with paginated devices."""
    query = """
    query GetSiteDevices($siteId: Int!, $limit: Int!, $offset: Int!) {
      site(id: $siteId) {
        name
        devices(limit: $limit, offset: $offset) {
          id
          name
          status
        }
      }
    }
    """

    all_devices = []
    offset = 0

    while True:
        response = requests.post(
            f"{netbox_url}/graphql/",
            headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json"
            },
            json={
                "query": query,
                "variables": {
                    "siteId": site_id,
                    "limit": devices_limit,
                    "offset": offset
                }
            }
        )

        data = response.json()["data"]["site"]
        devices = data["devices"]

        if not devices:
            break

        all_devices.extend(devices)

        if len(devices) < devices_limit:
            break

        offset += devices_limit

    return {
        "name": data["name"],
        "devices": all_devices
    }
```

## Query Optimizer Detection

The [netbox-graphql-query-optimizer](https://github.com/netboxlabs/netbox-graphql-query-optimizer) detects unbounded nested lists:

```bash
netbox-query-optimizer analyze query.graphql

# Output:
# Issues Found:
# - UNBOUNDED_LIST at line 4: 'devices' has no limit parameter
# - UNBOUNDED_LIST at line 6: 'interfaces' has no limit parameter
```

## Choosing Nested Limits

Consider the data distribution in your NetBox:

| Relationship | Typical Count | Suggested Limit |
|--------------|---------------|-----------------|
| Site → Devices | 10-500 | 50-100 |
| Device → Interfaces | 4-200 | 50-100 |
| Interface → IP Addresses | 1-5 | 10 |
| Prefix → Child Prefixes | 1-100 | 50 |
| VRF → Prefixes | 10-1000 | 100 |

## Exceptions

- **Single object:** `site(id: 123)` doesn't need pagination
- **Known small lists:** `tags` on an object (typically <10)
- **Scalar fields:** Non-list fields don't need pagination

## Related Rules

- [graphql-always-paginate](./graphql-always-paginate.md) - Top-level pagination
- [graphql-use-query-optimizer](./graphql-use-query-optimizer.md) - Detect issues
- [graphql-max-depth](./graphql-max-depth.md) - Limit nesting depth

## References

- [GraphQL Pagination Best Practices](https://graphql.org/learn/pagination/)
- [netbox-graphql-query-optimizer](https://github.com/netboxlabs/netbox-graphql-query-optimizer)

```

### references/rules/graphql-select-only-needed.md

```markdown
---
title: Request Only Needed Fields
impact: HIGH
category: graphql
tags: [graphql, performance, fields, optimization]
netbox_version: "4.4+"
---

# graphql-select-only-needed: Request Only Needed Fields

## Rationale

GraphQL's power is selecting exactly the fields you need. Over-fetching wastes:
- Database query time (joining unnecessary tables)
- Serialization time
- Network bandwidth
- Client parsing time

Only request fields your application actually uses.

## Incorrect Pattern

```graphql
# WRONG: Requesting all available fields "just in case"
query {
  device_list(limit: 100) {
    id
    url
    display
    name
    device_type {
      id
      url
      display
      manufacturer {
        id
        url
        display
        name
        slug
        description
      }
      model
      slug
      part_number
      u_height
      is_full_depth
      subdevice_role
      airflow
    }
    role {
      id
      url
      display
      name
      slug
      color
      vm_role
      description
    }
    tenant {
      id
      url
      display
      name
      slug
    }
    platform {
      id
      url
      display
      name
      slug
    }
    serial
    asset_tag
    site {
      id
      url
      display
      name
      slug
      status
      region {
        id
        url
        display
        name
        slug
      }
    }
    # ... 20+ more fields
  }
}
```

**Problems with this approach:**
- Fetches data never used
- Joins many related tables unnecessarily
- Large response payload
- Slower query execution

## Correct Pattern

```graphql
# CORRECT: Request only what you need
query GetDeviceList {
  device_list(limit: 100) {
    name
    status
    primary_ip4 {
      address
    }
  }
}
```

**Benefits:**
- Minimal data transfer
- Faster query execution
- Smaller response to parse
- Clear intent of data usage

## Determine Required Fields

Before writing a query, ask:
1. What will the UI/code actually display or process?
2. Which fields are used in logic/filtering?
3. Are nested fields really needed, or just IDs?

### Example Analysis

**Task:** Display a device table with name, status, and IP

**Required fields:**
- `name` - display in table
- `status` - display in table
- `primary_ip4.address` - display in table

**Not required:**
- `id` - unless linking to detail pages
- `device_type.manufacturer.description` - not displayed
- `site.region.slug` - not displayed

## Progressive Enhancement

Start minimal, add fields as needed:

```graphql
# Version 1: Basic list
query GetDevices {
  device_list(limit: 100) {
    name
    status
  }
}

# Version 2: Added IP after realizing we need it
query GetDevices {
  device_list(limit: 100) {
    name
    status
    primary_ip4 {
      address
    }
  }
}

# Version 3: Added site for grouping feature
query GetDevices {
  device_list(limit: 100) {
    name
    status
    primary_ip4 {
      address
    }
    site {
      name
    }
  }
}
```

## Field Selection for Different Use Cases

### Dashboard Widget (Minimal)

```graphql
query DashboardDeviceCounts {
  device_list(limit: 1) {
    id
  }
  active: device_list(filters: {status: "active"}, limit: 1) {
    id
  }
}
```

### Device List Table

```graphql
query DeviceTable {
  device_list(limit: 50) {
    id           # For row key/linking
    name         # Display column
    status       # Display column
    site {
      name       # Display column
    }
    primary_ip4 {
      address    # Display column
    }
  }
}
```

### Device Detail View

```graphql
query DeviceDetail($id: Int!) {
  device(id: $id) {
    name
    status
    serial
    asset_tag
    device_type {
      model
      manufacturer {
        name
      }
    }
    site {
      name
    }
    rack {
      name
    }
    position
    comments
  }
}
```

## Complexity Impact

Field selection affects query complexity scores:

```graphql
# Score: ~50 (good)
query {
  device_list(limit: 100) {
    name
    status
  }
}

# Score: ~250 (higher due to nested objects)
query {
  device_list(limit: 100) {
    name
    status
    device_type {
      model
      manufacturer {
        name
      }
    }
    site {
      name
      region {
        name
      }
    }
  }
}
```

## Exceptions

- **Caching:** May fetch extra fields if query results are cached and reused
- **Unknown requirements:** During prototyping, over-fetching may be acceptable
- **GraphQL fragments:** Shared fragments may include more fields than one use case needs

## Related Rules

- [graphql-always-paginate](./graphql-always-paginate.md) - Limit result counts
- [graphql-max-depth](./graphql-max-depth.md) - Limit nesting
- [rest-field-selection](./rest-field-selection.md) - REST equivalent

## References

- [GraphQL Best Practices](https://graphql.org/learn/best-practices/)

```

### references/rules/graphql-calibrate-optimizer.md

```markdown
---
title: Calibrate Query Optimizer Against Production
impact: HIGH
category: graphql
tags: [graphql, optimization, calibration, performance]
netbox_version: "4.4+"
---

# graphql-calibrate-optimizer: Calibrate Query Optimizer Against Production

## Rationale

The [netbox-graphql-query-optimizer](https://github.com/netboxlabs/netbox-graphql-query-optimizer) provides complexity scores, but default scores are estimates based on typical data distributions. Calibrating against your actual NetBox instance gives accurate scores based on real object counts.

Without calibration:
- Scores may underestimate actual complexity
- Queries that seem acceptable may cause problems
- Over-cautious limits may reject valid queries

## Incorrect Pattern

```bash
# Using only default scores without calibration
netbox-query-optimizer analyze query.graphql

# Output with defaults:
# Complexity Score: 150 (OK)
# But actual data may result in score of 15,000
```

**Problems with this approach:**
- Default estimates may not match your data
- Sites with 500+ devices will have different scores than average
- May approve queries that will timeout in production

## Correct Pattern

```bash
# CORRECT: Calibrate against your actual NetBox instance
netbox-query-optimizer analyze query.graphql \
  --calibrate \
  --url https://netbox.example.com \
  --token nbt_abc123.xxxxx

# Output with real data:
# Complexity Score: 2,450 (WARNING - exceeds recommended threshold)
#
# Calibration Data:
#   - site_list: 45 sites
#   - avg devices per site: 120
#   - avg interfaces per device: 48
```

**Benefits:**
- Accurate complexity based on your data
- Identifies queries that will perform poorly in production
- Enables informed decisions about query design

## Calibration Process

### Step 1: Install the Optimizer

```bash
pip install netbox-graphql-query-optimizer
```

### Step 2: Create Calibration Token

Create a read-only token for calibration queries:

1. In NetBox, create a user with read-only permissions
2. Generate an API token for that user
3. Store token securely (don't commit to repo)

### Step 3: Run Calibrated Analysis

```bash
# Set token as environment variable
export NETBOX_TOKEN="nbt_abc123.xxxxx"

# Analyze with calibration
netbox-query-optimizer analyze query.graphql \
  --calibrate \
  --url https://netbox.example.com \
  --token "$NETBOX_TOKEN"
```

### Step 4: Interpret Results

```
Query Analysis Report (Calibrated)
==================================

Data Profile:
  Total sites: 45
  Total devices: 5,400
  Total interfaces: 259,200
  Avg devices per site: 120.0
  Avg interfaces per device: 48.0

Query: GetSiteOverview

Uncalibrated Score: 150
Calibrated Score: 12,960

Issues:
  - FAN_OUT: 45 sites × 120 devices × 48 interfaces = 259,200 potential objects
  - EXCEEDS_BUDGET: Score 12,960 exceeds threshold 1,000

Recommendations:
  1. Add stricter limits: devices(limit: 10), interfaces(limit: 10)
  2. Split into multiple queries
  3. Consider REST API for simpler access patterns
```

## Calibration in CI/CD

Create a calibration job that runs against staging/production-like data:

```yaml
# .github/workflows/query-validation.yml
name: Validate GraphQL Queries

on: [push, pull_request]

jobs:
  validate-queries:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - run: pip install netbox-graphql-query-optimizer

      - name: Analyze queries with calibration
        env:
          NETBOX_TOKEN: ${{ secrets.NETBOX_READONLY_TOKEN }}
          NETBOX_URL: ${{ secrets.NETBOX_STAGING_URL }}
        run: |
          for query in queries/*.graphql; do
            echo "Analyzing $query..."
            netbox-query-optimizer analyze "$query" \
              --calibrate \
              --url "$NETBOX_URL" \
              --token "$NETBOX_TOKEN" \
              --max-score 500 \
              --fail-on-warning
          done
```

## When to Recalibrate

Recalibrate when:
- Significant data growth (e.g., adding new sites)
- Changing data distribution (e.g., consolidating sites)
- Before production deployment
- Quarterly for ongoing projects

## Storing Calibration Data

For faster CI runs, cache calibration data:

```bash
# Export calibration data
netbox-query-optimizer calibrate \
  --url https://netbox.example.com \
  --token "$NETBOX_TOKEN" \
  --output calibration.json

# Use cached calibration
netbox-query-optimizer analyze query.graphql \
  --calibration-file calibration.json
```

## Exceptions

- **Development:** Default scores are fine for local testing
- **Empty databases:** Calibration is meaningless without data
- **Rapidly changing data:** May need frequent recalibration

## Related Rules

- [graphql-use-query-optimizer](./graphql-use-query-optimizer.md) - Basic optimizer usage
- [graphql-complexity-budgets](./graphql-complexity-budgets.md) - Setting thresholds
- [graphql-pagination-at-each-level](./graphql-pagination-at-each-level.md) - Fix high scores

## References

- [netbox-graphql-query-optimizer](https://github.com/netboxlabs/netbox-graphql-query-optimizer)

```

### references/rules/graphql-max-depth.md

```markdown
---
title: Limit Query Depth
impact: HIGH
category: graphql
tags: [graphql, depth, nesting, performance]
netbox_version: "4.4+"
---

# graphql-max-depth: Limit Query Depth

## Rationale

Deep nesting in GraphQL queries causes exponential complexity growth. Each nesting level can multiply the number of database queries and objects returned.

**Guidelines:**
- Keep depth at **3 or below** for most queries
- **Never exceed 5** levels of nesting
- Consider REST or multiple queries for deeper data needs

## Incorrect Pattern

```graphql
# WRONG: Excessive nesting depth (5 levels)
query {
  site_list(limit: 10) {           # Level 1
    name
    devices(limit: 50) {            # Level 2
      name
      interfaces(limit: 100) {       # Level 3
        name
        ip_addresses(limit: 10) {    # Level 4
          address
          vrf {                      # Level 5 - TOO DEEP
            name
            route_targets {          # Level 6 - DANGER
              name
            }
          }
        }
      }
    }
  }
}
```

**Problems with this approach:**
- Complexity grows exponentially with depth
- N+1 query patterns at each level
- Difficult to optimize database queries
- Large memory footprint
- Risk of timeouts

## Correct Pattern

```graphql
# CORRECT: Depth limited to 3 levels
query GetSiteDeviceSummary {
  site_list(limit: 10) {           # Level 1
    name
    devices(limit: 20) {            # Level 2
      name
      interface_count              # Scalar, not nested
      primary_ip4 {                # Level 3 (max)
        address
      }
    }
  }
}
```

**Benefits:**
- Predictable performance
- Manageable complexity
- Easier to optimize

## Split Deep Queries

Instead of one deep query, use multiple shallow queries:

```graphql
# Query 1: Get sites and devices (depth: 2)
query GetSitesAndDevices {
  site_list(limit: 10) {
    id
    name
    devices(limit: 20) {
      id
      name
    }
  }
}

# Query 2: Get interfaces for specific device (depth: 2)
query GetDeviceInterfaces($deviceId: Int!) {
  interface_list(device_id: $deviceId, limit: 100) {
    id
    name
    ip_addresses(limit: 10) {
      address
    }
  }
}

# Query 3: Get VRF details if needed (depth: 2)
query GetVRFDetails($vrfId: Int!) {
  vrf(id: $vrfId) {
    name
    description
    import_targets(limit: 10) {
      name
    }
    export_targets(limit: 10) {
      name
    }
  }
}
```

## Depth Counting

Count from the root list/object:

```graphql
query {
  site_list {        # Level 1 (root)
    name
    devices {         # Level 2
      name
      interfaces {     # Level 3
        name
        ip_addresses { # Level 4 (AVOID)
          address
        }
      }
    }
  }
}
```

**Note:** Scalar fields (name, status, etc.) don't add depth. Only nested objects/lists count.

## When Depth is Acceptable

Some shallow depth-4 queries may be acceptable with proper pagination:

```graphql
# Acceptable: depth 4 but well-constrained
query {
  site_list(limit: 5) {
    name
    devices(limit: 10) {
      name
      interfaces(limit: 5) {  # Only 5 interfaces
        name
        ip_addresses(limit: 2) {  # Only 2 IPs
          address
        }
      }
    }
  }
}
# Max objects: 5 × 10 × 5 × 2 = 500 (manageable)
```

## REST Alternative for Deep Data

When you need deeply nested data, REST may be simpler:

```python
import requests

# Fetch devices
devices_resp = requests.get(
    f"{API_URL}/dcim/devices/?site=nyc-dc1&limit=20",
    headers=headers
)
devices = devices_resp.json()["results"]

# For each device, fetch interfaces
for device in devices:
    interfaces_resp = requests.get(
        f"{API_URL}/dcim/interfaces/?device_id={device['id']}&limit=100",
        headers=headers
    )
    device["interfaces"] = interfaces_resp.json()["results"]

    # For each interface, IP addresses are already included
    # Or fetch separately if needed
```

## Query Optimizer Detection

The query optimizer detects depth violations:

```bash
netbox-query-optimizer analyze query.graphql

# Output:
# Issues Found:
# - DEPTH_EXCEEDED: Query depth 5 exceeds recommended max of 3
# - DEPTH_EXCEEDED: Query depth 6 at 'route_targets' is excessive
```

## Exceptions

- **Specific single-object queries:** `device(id: 123)` with nested details is often fine
- **Aggregate/count fields:** These don't add the same complexity as nested lists
- **Well-paginated queries:** Strict limits at every level can make depth-4 acceptable

## Related Rules

- [graphql-pagination-at-each-level](./graphql-pagination-at-each-level.md) - Limit nested counts
- [graphql-use-query-optimizer](./graphql-use-query-optimizer.md) - Detect depth issues
- [graphql-vs-rest-decision](./graphql-vs-rest-decision.md) - When to use REST instead

## References

- [GraphQL Best Practices](https://graphql.org/learn/best-practices/)
- [netbox-graphql-query-optimizer](https://github.com/netboxlabs/netbox-graphql-query-optimizer)

```

netbox-integration-best-practices | SkillHub