Skip to main content
Version: Next

Typed models

Resource client methods return values typed with TypeScript types generated directly from the Apify OpenAPI specification. The client also validates each response at runtime against a zod schema generated from the same specification. You get IDE autocompletion and compile-time checks, and the value you receive at runtime matches its type.

Accessing response fields

Every method that returns a structured payload resolves to a typed object, such as Actor, ActorRun or Dataset. Field names are the camelCase names the API uses, and the type of each field comes through to your editor.

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'MY-APIFY-TOKEN' });

// `get()` resolves to an `Actor`, or to `undefined` when the Actor doesn't exist.
const actor = await client.actor('apify/hello-world').get();

if (actor) {
console.log(actor.id); // string
console.log(actor.username); // string
console.log(actor.isPublic); // boolean
console.log(actor.createdAt); // Date
console.log(actor.stats.totalRuns); // number, nested objects are typed all the way down
}

Fields the specification declares as a date-time are converted into Date objects wherever they sit in the response, and nested objects get their own types, so you can compose field access without manual conversion. Fields the API can omit or return as null are typed as optional or nullable, so the compiler points out where a check is needed.

URL fields

Fields the specification marks as a URL, such as ActorRun.containerUrl or Dataset.consoleUrl, are parsed with the WHATWG URL parser during validation, and the client returns the parsed URL's serialization. The string you read back can therefore differ from the one the API sent. Both forms denote the same URL under RFC 3986, they're just different strings.

// An empty path becomes '/'.
new URL('https://abc123.runs.apify.net').href; // 'https://abc123.runs.apify.net/'

// The host is lowercased.
new URL('https://EXAMPLE.com/Path').href; // 'https://example.com/Path'

// A default port is dropped.
new URL('https://example.com:443/path').href; // 'https://example.com/path'

// An internationalized host is punycoded.
new URL('https://www.žluty.cz').href; // 'https://www.xn--luty-kbb.cz/'

// Unsafe characters are percent-encoded.
new URL('https://example.com/a b').href; // 'https://example.com/a%20b'

Because of the normalization, compare URLs in their normalized form, not as raw strings:

const run = await client.run('my-run-id').get();
const storedUrl = 'https://abc123.runs.apify.net';

// Wrong, compares a raw string with a normalized one.
storedUrl === run?.containerUrl; // false

// Right, both sides are normalized before the comparison.
new URL(storedUrl).href === run?.containerUrl; // true

To build a longer URL out of a URL field, use new URL(path, base) only when the field ends with a slash. A relative reference replaces the base's last path segment, so on consoleUrl it would drop the resource ID.

new URL('status', run?.containerUrl).href; // 'https://abc123.runs.apify.net/status'

For the full list of URL fields, see URL fields are normalized.

Response validation

A response that doesn't match the specification throws a ResponseValidationError. For how to handle it, see Invalid responses.

The check catches the API and its specification disagreeing: a missing required field, a different type, or a value outside the documented range. Such a mismatch would otherwise surface later as an undefined where the types promise a value. If you run into one, please report it.

Providing structured input

Every input a method takes has an exported type, such as ActorStartOptions or RequestQueueClientRequestToAdd, which lists the fields the API accepts. Annotate an object you build ahead of the call with it, and the compiler checks the object where you build it.

import { ApifyClient } from 'apify-client';
import type { RequestQueueClientRequestToAdd } from 'apify-client';

const client = new ApifyClient({ token: 'MY-APIFY-TOKEN' });

// The input type lists the fields the API accepts, so your editor completes and checks them.
const request: RequestQueueClientRequestToAdd = {
url: 'https://example.com',
uniqueKey: 'https://example.com',
method: 'GET',
};

await client.requestQueue('REQUEST-QUEUE-ID').addRequest(request);

The client validates the input at runtime too, so plain JavaScript callers get an ArgumentValidationError for a value of the wrong shape. Most input schemas reject unknown fields, so pass only the fields the input type lists. For details, see Invalid arguments.

Forward compatibility

The response schemas let fields the specification doesn't describe pass through untouched, and accept enum values it doesn't list. New fields the API starts returning are preserved on the object, they just don't have a declared type yet. Upgrading the client to pick up a newer specification doesn't break code that reads existing fields.

Browsing all types

The full list of exported types is available in the API reference.

Methods that return plain types

A few endpoints return values the client doesn't validate, because their payloads are user-defined or inherently unstructured:

To type the items of a dataset whose shape you know, pass the type to client.dataset():

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'MY-APIFY-TOKEN' });

// The shape of the items the Actor stores, taken from its output schema.
interface Product {
title: string;
price: number;
}

// The type parameter types the items. The client doesn't validate them.
const { items } = await client.dataset<Product>('dataset-id').listItems();

for (const { title, price } of items) {
console.log(`${title}: ${price}`);
}

For background on the move to generated types and response validation, see Upgrading to v3.