# Route Directives

Directives are declarative decorators available to route definitions. They provide additional functionality to the route definitions.

There are a number of active directives available below:

1. Caching
2. Rate Limiting
3. Elevated Privileges
4. Idempotency
5. Verified
6. Subscribed

## Caching

Routes can be cached by adding this directive in the route definition.

<pre class="language-typescript" data-title="app/api/accounts/index.ts" data-line-numbers><code class="lang-typescript">export default async function Route (instance: FastifyInstance) {
    instance.get('/api/v1/account', handler);
    <a data-footnote-ref href="#user-content-fn-1">instance.cached();</a>
    
    async function handler(request, reply) {
        console.log(request.session);
        reply.status(204);
    }
}
</code></pre>

The directive adds an additional header to the response based on the cached behaviour

`x-cache: hit` if the content is cached

`x-cache: miss` if the content is supposed to be cached, but wasnt.

### `instance.cached()`

The default behaviour and will cache the request based on the user, url, and session of the user.

### ``instance.cached({ key: (request) ⇒ `custom-id` })``&#x20;

This overrides the key assigned in the caching table. The current request is available for use to derive a specific id.

### `instance.cached({ ttl: 60 })`

This changes the ttl (Time to live) of the cached object. This ttl is `5`seconds by default.

### `await reply.uncache(key: string, options?: CacheOptions)`

This is a decorator to reply that you can use to manually purge a cached response.

### `CacheOptions`

* ttl - Time to live of the cached object. default`5`seconds
* key - (request: Request) function to generate a cache key

## Rate Limiting

Rate limiting is provided by <https://github.com/fastify/fastify-rate-limit>

### `instance.throttled(limit: number, window: string, options?: RateLimitOptions)`

This is a separate directive that has a different signature from the throttling shown after. You can use it like any other directive

{% code title="app/api/route/index.ts" lineNumbers="true" %}

```typescript
export default function Route(instance: FastifyInstance) {
    instance.get('/api/v1/account', handler);
    instance.throttled();
    
    async function handler(request, reply) {
        reply.status(204);
    }
}
```

{% endcode %}

### `await request.throttled(key: string, limit?: number, options?: RateLimitOptions)`

### `await request.throttled(limit: number, options?: RateLimitOptions)`

The signature for throttled allow different usage for convenience.

{% code title="app/api/route/index.ts" lineNumbers="true" %}

```typescript
export default function Route(instance: FastifyInstance) {
    instance.get('/api/v1/account', handler);
    
    async function handler(request, reply) {
        await request.throttled('throttled');
        reply.status(204);
    }
}
```

{% endcode %}

### `await request.unthrottle(key?: string, options?: RateLimitOptions)`

If you need to explicitly reset the rate limit for a specific key.&#x20;

{% code title="app/api/route/index.ts" lineNumbers="true" %}

```typescript
export default function Route(instance: FastifyInstance) {
    instance.get('/api/v1/account', handler);
    
    async function handler(request, reply) {
        await request.throttled('throttled');
        reply.status(204);
        await request.unthrottle('throttled');
    }
}
```

{% endcode %}

Example usage: [auth-passwordless/index.ts](https://github.com/madewithnovel/novel/blob/main/app/api/internal/v1/auth-passwordless/index.ts#L25)

### `RateLimitOptions`

* timeWindow - the time in seconds on how long the rate limit should last
* max - the maximum amount before the throttling kicks in
* session - custom option used in generating the key

## Elevated Privileges

If you require actions to be verified by the user again, you can make use of the sudo middleware that exposes a password check before letting the action go through.

### `instance.sudo()`

<pre class="language-typescript" data-title="app/api/route/index.ts" data-line-numbers><code class="lang-typescript">export default async function Route(instance: FastifyInstance) {
    instance.post('/api/v1/accounts', handler);
    <a data-footnote-ref href="#user-content-fn-2">instance.sudo();</a>
    
    async function handler(request, reply) {
        console.log(request);
        reply.status(204);
    }
}
</code></pre>

{% hint style="warning" %}
**Important Note**

You need to add this piece of code in your schema so it can be picked up by Novel Web
{% endhint %}

{% code title="app/api/route/schema.json" lineNumbers="true" %}

```json
{
    ...rest of your schema.json
    "body": {
	"type": "object",
	"required": [...other fields, "sudo_password"],
	"properties": {
		...other fields
		"sudo_password": {
			"type": "string"
		}
	}
    }
}
```

{% endcode %}

See [Schema →](/novel-server/routing/schema.md)

## Idempotency

If you want to enable idempotency for a route. You can add the idempotency directive for that definition

{% code title="app/api/route/index.ts" lineNumbers="true" %}

```typescript
export default async function Route(instance: FastifyInstance) {
    instance.post('/api/v1/accounts', handler);
    instance.idempotent();
    
    async function handler(request, reply) {
        // or await request.idempotent();
        console.log(request.headers['idempotency-key']);
        reply.status(204);
    }
}
```

{% endcode %}

Requests to this endpoint need to have a unique `idempotency-key`header generated by the client.

{% code lineNumbers="true" %}

```javascript
await fetch('/api/v1/accounts', {
    method: 'POST',
    headers: {
        'idempotency-key': uuid.v1(),
    },
});
```

{% endcode %}

### `instance.idempotent(options?: IdempotencyOptions)`

If the endpoint has processed the request and a similar request with the same idempotency key is sent, it will return the response of the previous action.

### `IdempotencyOptions`

* ttl - the amount of time before the idempotency key can be reused. default 86400

## Verified

### `instance.verified()`

Check if the currently logged in user has a `verified`status.

{% code title="app/api/v1/your-route/index.ts" lineNumbers="true" %}

```typescript
export default function Route(instance) {
    instance.authorized();
    instance.verified();
    instance.get('/your/route', handler);
    
    async function handler (request, reply) {
        reply.send('ONLY FOR AUTHENTICATED API KEYS');
    }
}
```

{% endcode %}

## Subscribed

### `instance.subscribed()`

Check if the currently logged in organization has an active subscription.

{% code title="app/api/v1/your-route/index.ts" lineNumbers="true" %}

```typescript
export default function Route(instance) {
    instance.authorized();
    instance.subscribed();
    instance.get('/your/route', handler);
    
    async function handler (request, reply) {
        reply.send('ONLY FOR SUBSCRIBED ORGANIZATIONS');
    }
}
```

{% endcode %}

## Changelog

* 2024-12-20 - Initial Documentation

[^1]: This is the caching directive

[^2]: will look for `sudo_password`in the request body.


---

# Agent Instructions: Querying This Documentation

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

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

```
GET https://docs.novel.dev/novel-server/routing/route-directives.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

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