Skip to content

defineApi()

The defineApi() function is used to create a composable that communicates with an external server through endpoints.

Create API Composable

Creating an API composable for a model is similar to defining a Pinia store.

typescript
const useModelApi = defineApi(Model);

For example:

typescript
import { Model, defineApi } from '@kovalson/prevue';

class User extends Model {
  // ...
}

const useUserApi = defineApi(User);

export {
  User,
  useUserApi,
};

Setup

You may pass a setup object as a second parameter to configure the composable.

typescript
import { Model, defineApi } from '@kovalson/prevue';

class User extends Model {
  // ...
}

const useUserApi = defineApi(User, {
  apiUrl: 'https://my.domain/api',
  uri: 'users',
  endpoints: {
    async active() {
      return await this.request('/active');
    },
  },
});

export {
  User,
  useUserApi,
};

TIP

Note that the uri, when not provided, is automatically derived from model's class name. It is then converted to kebab-case and the last segment is pluralized. For the example above, the uri automatically becomes 'users'.

The setup option helps you customize the uri value if it does not match your model's class name or if it is too complicated for the automated function to process.

Setup Object

typescript
interface ISetupObject {
  /**
   * The base API url of the target server.
   * 
   * @default undefined
   */
  apiUrl?: string;
  
  /**
   * The resource URI.
   * 
   * This value suffixes the API url. When not provided, it is automatically derived from model's class name.
   * 
   * @default undefined
   */
  uri?: string;

  /**
   * Whether to convert multiple consecutive occurrences of slashes (/) to single uses in the constructed URL.
   * 
   * @example
   *  https://example.api//users
   *  becomes
   *  https://example.api/users
   * 
   * @default true
   */
  trimSlashes?: boolean;

  /**
   * Custom endpoints that enhance the API functionality.
   * 
   * @default undefined
   */
  endpoints?: Record<string, TCustomEndpoint>;

  /**
   * The pagination template for responses.
   *
   * @default undefined
   */
  pagination?: IPaginationSetup;

  /**
   * The request options.
   * 
   * @default undefined
   */
  options?: TApiOptions;
}
typescript
import { FetchOptions } from 'ofetch';

type TApiOptions = FetchOptions<'json'>;

Custom Endpoints

Custom endpoints are functions that are automatically bound a special context. This context provides them with all other endpoints (so you might call endpoints from within other endpoints) as well as the base request() and rawRequest() methods.

You may, or may not make use of custom endpoint context. You are free to return anything from your method, but bear in mind that the API should be used only to perform requests and return sanitized, model-converted and wrapped responses.

request()

This special method performs an HTTP request using ofetch package. It automatically converts response data into the corresponding model (or models) and detects pagination when wrapping up the response to ApiResponse object. All default methods utilize request() function with different parameters under the hood.

rawRequest()

This special method performs the lowest-level, raw request. It is still constructed using apiUrl and uri under the hood, mostly for convenience, and can be further configured with options parameter.

typescript
interface ICustomEndpointContext {
  request<T = any>(url: string, options?: TApiOptions): Promise<T>;
}

Note that the return type is any by default, and can (and surely should) be changed when using the method.

The following is an example of implementing a custom endpoint that toggles user's active state.

typescript
import { Model, SimpleResponse, createInstance, defineApi } from '@kovalson/prevue';

class User extends Model {
  readonly id: string = '';
  readonly active: boolean = false;
}

const useUserApi = defineApi(User, {
  apiUrl: 'https://api.domain',
  endpoints: {
    async activate(id: string): Promise<User> {
      const response = await this.rawRequest(`${id}/activate`, {
        method: 'POST',
      });
      return SimpleResponse(createInstance(User, response));
    },
  },
});

// Now use the composable to create API instance
const userApi = useUserApi();

// And finally call the endpoint
userApi.activate('some-user-id');

Request Options

The default requests, as well as the mentioned request() and rawRequest() methods, have an additional options parameter (optional). These options type comes from the ofetch package to enable easy configuration of the request.

typescript
import type { FetchOptions } from 'ofetch';

type TApiOptions = FetchOptions<'json'>;

Default Endpoints

The created API composable has several BREAD endpoints out-of-the-box.

Let's assume we have the following configuration:

typescript
const useUserApi = defineApi(User, {
  apiUrl: 'https://api.domain',
});

fetchAll()

Calls the index endpoint for given resource.

http
GET https://api.domain/users

Example

typescript
userApi.fetchAll().then((response) => {
  // ...
});

fetchOne()

Calls the show endpoint for given resource's id.

http
GET https://api.domain/users/some-id

Example

typescript
userApi.fetchOne('some-id').then((response) => {
  // ...
});

fetchMany()

Calls the show endpoint for multiple given resources' ids.

http
GET https://api.domain/users

with body payload:

json
{
  "ids": [
    "some-id-1",
    "some-id-2"
  ]
}

Example

typescript
userApi.fetchMany(['some-id-1', 'some-id-2']).then((response) => {
  // ...
});

createOne()

Calls the store endpoint for given resource.

http
POST https://api.domain/users

Example

typescript
userApi.createOne({
  // ...
}).then((response) => {
  // ...
});

updateOne()

Calls the update endpoint for given resource.

http
PATCH https://api.domain/users/some-id

Example

typescript
userApi.updateOne('some-id', {
  // ...
}).then((response) => {
  // ...
});

updateMany()

Calls the store endpoint for given resources.

http
PATCH https://api.domain/users

with body payload

json5
{
  "ids": [
    "some-id-1",
    "some-id-2"
  ],
  "data": {
    // ...
  }
}

Example

typescript
userApi.updateMany(['some-id-1', 'some-id-2'], {
  // ...
}).then((response) => {
  // ...
});

deleteOne()

Calls the destroy endpoint for given resource.

http
DELETE https://api.domain/users/some-id

Example

typescript
userApi.deleteOne('some-id').then(() => {
  // ...
});

deleteMany()

Calls the destroy endpoint for given resources' ids.

http
DELETE https://api.domain/users

with body payload

json
{
  "ids": [
    "some-id-1",
    "some-id-2"
  ]
}

Example

typescript
userApi.deleteMany(['some-id-1', 'some-id-2']).then(() => {
  // ...
});

Pagination

Prevue automatically detects paginated responses and unwraps the incoming object.

If the response is paginated, the object returned by the request function inherits from PaginatedResponse class. Otherwise, the object inherits from SimpleResponse class.

The controller, on the other hand, returns an array of models (as it would by default), but also saves the pagination data into a separate pagination property available from the controller object.

The following is an example of handling paginated response by using the controller.

typescript
const { fetchAll, pagination } = useUserController();

await fetchAll();

console.log(pagination.total);

Default Template

By default, the following template is applied to detect a paginated response:

json5
{
  "data": [
    // Array of objects
  ],
  "pagination": {
    "currentPage": 0,
    "lastPage": 0,
    "perPage": 0,
    "total": 0
  }
}

Custom Template

You can define a custom pagination template using the pagination setup property:

typescript
import { defineConfig } from '@kovalson/prevue';

const useUserApi = defineApi(User, {
  pagination: {
    paginationWrapper: 'pages',
    dataWrapper: 'items',
    currentPage: 'current',
    lastPage: 'last',
    perPage: 'per_page',
    total: 'count',
  },
});

If you need more control over the response, you can use a pagination response mapper function:

typescript
import { defineConfig } from '@kovalson/prevue';

const useUserApi = defineApi(User, {
  pagination: {
    dataWrapper: 'items',
    mapper(response) {
      return {
        currentPage: response.pages.current,
        lastPage: Math.ceil(response.pages.meta.resultsCount / response.pages.current * response.pages.perPage),
        perPage: 10,
        total: response.pages.meta.resultsCount,
      };
    },
  },
});

Response Object

Default endpoints return responses wrapped in one of the following classes: SimpleResponse or PaginatedResponse. It is strongly recommended to utilize these classes when creating custom endpoints as some Prevue's internal features need this detail specified.

Both response classes extend the same base ApiResponse class and share all its methods and properties. The only difference is that the isPaginated() method returns true for PaginatedResponse. Obviously, the getPagination() returns undefined for SimpleResponse.

Custom API Response

As stated before, you should always wrap your API responses, so that you can fully benefit from Prevue's functionality. Wrapping response is simple:

typescript
const useUserApi = defineApi(User, {
  endpoints: {
    async activate(id: string): Promise<SimpleResponse<User>> {
      const response = await this.rawRequest(`/${id}/activate`, {
        method: 'POST',
      });
      return new SimpleResponse(createInstance(User, response));
    },
  },
});

or, when pagination is present:

typescript
const useUserApi = defineApi(User, {
  endpoints: {
    async fetchActive(): Promise<PaginatedResponse<User[]>> {
      const data = await this.rawRequest(`/active`, {
        method: 'GET',
      });
      return new PaginatedResponse(data.users.map((user) => createInstance(User, user)), data.pagination);
    },
  },
});

request() vs rawRequest()

As mentioned before, request() is a high-level function that performs an HTTP request and automatically takes care of converting and sanitizing the response. The rawRequest() function, on the other hand, performs a request and returns the original response, untouched. It therefore requires a bit more verbosity, but also gives full control over the request and the response.

The following is an example of how you might implement the same custom endpoint with each of these functions. The activate() method should perform a request and return an updated user object:

typescript
import { Model, SimpleResponse, createInstance, defineApi } from '@kovalson/prevue';

class User extends Model {
  // ...
}

const useUserApi = defineApi(User, {
  endpoints: {
    async activate(id: string) {
      const response = await this.rawRequest(`/${id}/activate`, {
        method: 'POST',
      });
      return new SimpleResponse(createInstance(User, response));
    },
  },
});

export {
  User,
  useUserApi,
};
typescript
import { Model, defineApi } from '@kovalson/prevue';

class User extends Model {
  // ...
}

const useUserApi = defineApi(User, {
  endpoints: {
    async activate(id: string) {
      return await this.request(`/${id}/activate`, {
        method: 'POST',
      });
    },
  },
});

export {
  User,
  useUserApi,
};

The request() method is especially useful when dealing with pagination. Let's assume we need to fetch paginated users list.

typescript
import { Model, PaginatedResponse, createInstance, defineApi } from '@kovalson/prevue';

class User extends Model {
  // ...
}

const useUserApi = defineApi(User, {
  endpoints: {
    async paginated() {
      const response = await this.rawRequest('/paginated', {
        method: 'GET',
      });
      return new PaginatedResponse(
        response.data.map((data) => createInstance(User, data)),
        {
          total: response.data.pagination.total,
          currentPage: response.data.pagination.currentPage,
          // ...
        },
      );
    },
  },
});

export {
  User,
  useUserApi,
};
typescript
import { Model, defineApi } from '@kovalson/prevue';

class User extends Model {
  // ...
}

const useUserApi = defineApi(User, {
  endpoints: {
    async paginated() {
      return await this.request('/paginated', {
        method: 'GET',
      });
    },
  },
});

export {
  User,
  useUserApi,
};

Released under the MIT License.