npm package: @kyvan2x/payload-rest-client
Description: A typesafe rest api client for the payload cms.
Author: yvankanaj@gmail.com
Latest version: 1.0.0

# payload-rest-client

A typesafe rest api client for the [payload cms](https://payloadcms.com).

## Quick Start 

1. Assume you have a users (auth enabled) and a posts collection with following fields:

```ts
interface User {
    id: string;
    email: string;
    name: string;
    password: string;
    createdAt: string;
    updatedAt: string;
}

interface Post {
    id: string;
    title: string;
    content: string;
    createdAt: string;
    updatedAt: string;
}
```

2. Create the client:

```ts
import { createClient } from "@kyvan2x/payload-rest-client";
import { Config } from "./payload-types"; // auto generated types from payload

type Locales = "de" | "en";

const client = createClient<Config, Locales>({
    apiUrl: "http://localhost:4000/api",
});
```

3. Now you can use all available queries for all collections and globals in a typesafe way:

```ts
// if you wan't to use protected routes, use login api...
const loginResponse = await client.collections.users.login({
    email: process.env.PAYLOAD_API_EMAIL,
    password: process.env.PAYLOAD_API_PASSWORD,
});

// ...and create another client with authorization header
const protectedClient = createClient<Config, Locales>({
    apiUrl: "http://localhost:4000/api",
    headers: {
        "Authorization": `Bearer ${loginResponse.token}`,
    },
});

const posts = await protectedClient.collections.posts.find({
    sort: "title", // only top level keys (optionally prefixed with "-") of Post allowed
    locale: "de", // only defined locales allowed
    limit: 10,
    page: 2,
});

console.log(posts); // type of posts is FindResult<Post> 
```

## Error Handling

The client provides two approaches for error handling:

### 1. Traditional try/catch approach

```ts
import { 
    PayloadClientError, 
    ValidationError, 
    UnauthorizedError, 
    NotFoundError 
} from "@kyvan2x/payload-rest-client";

try {
    const user = await client.collections.users.create({
        doc: { email: "invalid-email", name: "Test" }
    });
} catch (error) {
    if (error instanceof ValidationError) {
        // Get structured validation errors
        const fieldErrors = error.getFieldErrors();
        console.log("Field errors:", fieldErrors);
        
        // Get full error data
        const errorData = error.getErrorData();
        console.log("Error details:", errorData);
    } else if (error instanceof UnauthorizedError) {
        console.log("Authentication required");
    } else if (error instanceof PayloadClientError) {
        console.log(`API Error (${error.statusCode}):`, error.message);
        console.log("Response data:", error.data);
    }
}
```

### 2. Functional error handling with `client.safe`

For a more functional approach without try/catch blocks, use the `safe` version of the client:

```ts
// Using the safe client - returns [error, undefined] or [undefined, result]
const [error, user] = await client.safe.collections.users.create({
    doc: { email: "invalid-email", name: "Test" }
});

if (error) {
    if (error instanceof ValidationError) {
        const fieldErrors = error.getFieldErrors();
        console.log("Validation failed:", fieldErrors);
    } else {
        console.log("Error:", error.message);
    }
    return;
}

// user is guaranteed to be defined here
console.log("User created:", user.doc);
```

### 3. Using the standalone `tryCatch` utility

You can also use the `tryCatch` utility function directly:

```ts
import { tryCatch, ValidationError, UnauthorizedError } from "@kyvan2x/payload-rest-client";

// Catch specific error types
const [error, result] = await tryCatch(
    client.collections.users.create({ doc: userData }),
    [ValidationError, UnauthorizedError]
);

if (error) {
    // Handle specific errors
    console.log("Caught expected error:", error.message);
} else {
    // Success case
    console.log("User created:", result.doc);
}

// Catch all errors
const [anyError, posts] = await tryCatch(
    client.collections.posts.find()
);

if (anyError) {
    console.log("Something went wrong:", anyError.message);
} else {
    console.log("Posts:", posts.docs);
}
```

### Error Types

- `PayloadClientError`: Base error class with `statusCode`, `data`, and `response` properties
- `ValidationError` (400): Validation errors with field-specific details
- `UnauthorizedError` (401): Authentication required
- `ForbiddenError` (403): Access denied
- `NotFoundError` (404): Resource not found
- `ConflictError` (409): Resource conflict
- `ServerError` (500+): Server-side errors

## Custom Endpoints

1. Define input and output types for alle custom endpoints:

```ts
import { CustomEndpoint } from "@kyvan2x/payload-rest-client";

/**
 * shape of generic CustomEndpoint type
 *
 * type Input = {
 *     params?: Record<string, string>;
 *     query?: Record<string, any>;
 *     body?: any;
 * };
 *
 * type Output = any;
 *
 * type CustomEndpoint<Input, Output>;
 */

type CustomEndpoints = {
    greet: CustomEndpoint<{
        params: { name: string };
        query: { locale: Locales };
    }, string>,
};
```

2. Add it to `createClient` function:

```diff
-const client = createClient<Config, Locales>({
-    apiUrl: "http://localhost:4000/api",
-});
+const client = createClient<Config, Locales, CustomEndpoints>({
+    apiUrl: "http://localhost:4000/api",
+    customEndpoints: {
+        greet: { method: "GET", path: params => `hello/${params.name}` },
+    },
+});
```

3. Call custom endpoints like this:

```ts
// Traditional approach
try {
    const greeting = await client.custom.greet({
        params: { name: "John Doe" },
        query: { locale: "en" },
    });
    console.log(greeting);
} catch (error) {
    console.error("Failed to greet:", error);
}

// Safe approach
const [error, greeting] = await client.safe.custom.greet({
    params: { name: "John Doe" },
    query: { locale: "en" },
});

if (error) {
    console.error("Failed to greet:", error.message);
} else {
    console.log("Greeting:", greeting);
}
```

## API

[Full documentation of the rest api](https://payloadcms.com/docs/rest-api/overview)

### Client options

- `apiUrl`: string - Base URL for the Payload API
- `cache?`: RequestCache - Cache strategy for requests
- `headers?`: HeadersInit - Default headers to include with requests
- `debug?`: boolean - Enable debug logging
- `getAdditionalFetchOptions?`: (params: GetAdditionalFetchOptionsParams) => any - Function to add additional fetch options
- `customFetchFn?`: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> - Custom fetch function
- `customEndpoints?`: Record<String, CustomEndpointFactory> - Custom endpoint definitions

### Collections

- `find`: (params?: FindParams<T, LOCALES>) => Promise<FindResult<T>>
- `findById`: (params: FindByIdParams<LOCALES>) => Promise<T>
- `count`: (params?: CountParams<T>) => Promise<CountResult>
- `create`: (params: CreateParams<T, LOCALES>) => Promise<CreateResult<T>>
- `createDraft`: (params: CreateDraftParams<T, LOCALES>) => Promise<CreateDraftResult<T>>
- `update`: (params: UpdateParams<T, LOCALES>) => Promise<UpdateResult<T>>
- `updateById`: (params: UpdateByIdParams<T, LOCALES>) => Promise<UpdateByIdResult<T>>
- `delete`: (params?: DeleteParams<T, LOCALES>) => Promise<DeleteResult<T>>
- `deleteById`: (params: DeleteByIdParams<LOCALES>) => Promise<T>

### Collections with auth enabled (additional to above)

- `login`: (params: LoginParams) => Promise<LoginResult<T>>
- `logout`: (params?: LogoutParams) => Promise<LogoutResult>
- `unlock`: (params: UnlockParams) => Promise<UnlockResult>
- `refresh-token`: (params?: RefreshTokenParams) => Promise<RefreshTokenResult>
- `me`: (params?: MeParams) => Promise<MeResult<T>>
- `forgot-password`: (params: ForgotPasswordParams) => Promise<ForgotPasswordResult>
- `reset-password`: (params: ResetPasswordParams) => Promise<ResetPasswordResult<T>>

### Globals

- `get`: (params?: BaseParams<LOCALES>) => Promise<T>
- `update`: (params: UpdateGlobalParams<T, LOCALES>) => Promise<T>

### Others

- `access`: () => Promise<AccessResult>

## Changelog

### v 3.0.7

- **NEW**: Added functional error handling with `client.safe` - no more try/catch needed!
- **NEW**: Added `tryCatch` utility function for manual error handling
- **NEW**: All client methods now available in both traditional and safe versions
- **BREAKING**: Client now returns an object with both regular and safe APIs
- Added: Complete TypeScript support for safe error handling patterns
- Added: Comprehensive examples for both error handling approaches

### v 3.0.6

- **BREAKING**: Improved error handling with structured error types
- **BREAKING**: Better query string encoding with proper URL encoding
- **BREAKING**: Enhanced type safety for auth endpoints (optional parameters)
- Fixed: JSON error responses are now properly parsed and preserved as objects
- Fixed: Content-Type header is only added when there's a request body
- Fixed: Better handling of network errors and malformed responses
- Fixed: Improved debug logging with more detailed information
- Added: Comprehensive error types with field-specific validation errors
- Added: Better handling of array parameters in query strings
- Added: Input validation for client options

### v 3.0.5

- Infer id params (`string` or `number`) from type.

### v 3.0.4

- Added custom endpoints

### v 3.0.3

- Added option to use custom fetch function

### v 3.0.2

- Export error types
- Added access api

### v 3.0.1

- Better type inference for joins

### v 3.0.0

- Payload 3 (for Payload 2 use older versions)
- Added `select`, `populate` and `join` params
- Added `count` api