Apity: A typed HTTP client for Svelte(Kit)
Tags: svelte, javascript, api, frontend
Published: 2023-04-12 15:55:06

Most frontend projects need to make HTTP requests, but there's no convention how to handle them. We have a `fetch` function, but it would be inconvenient to call it directly each time, so often developers create their own API wrappers to call logical functions instead, for example:

```js
const products = await API.fetchProducts({ userId })
// or
const response = await listProducts({ userId })
```

I've seen a lot of code like this. Sometimes backend is implemented with the same technology as frontend and a framework provides helpers for network communication, for example SvelteKit has [form actions](https://kit.svelte.dev/docs/form-actions). But in certain cases backend is a separate instance and you operate with its API, so such functions are unavoidable.

The main question with this approach is how to manage request parameters and data in responses. If you're a TypeScript user then you know the benefits of proper typing. TS warns you when you're trying to access wrong properties of objects and a lot of mistakes can be caught even before running the code. And of course having auto completion in your IDE makes development experience much better. So by returning a raw `response.json()` you lose a lot of benefits.

However, there's a couple of issues with this approach:

1) Writing the types by hand is tedious and error prone
2) Backend changes over time, it can be a new property added to a response, or a new parameter in request body - so you have to keep both codebases in sync
3) Sometimes the types are implemented by backend team already, so duplicating them on frontend sounds like a double work

Combining all these concerns together and trying to find a proper solution resulted in [Apity](https://github.com/cocreators-ee/apity) - a Svelte/SvelteKit library for making typed HTTP requests. In this article I'll try to show its bright sides and how to use it.

## Installation and configuration

A pre-requisite before taking Apity into use is having an [OpenAPI](https://swagger.io/specification/) spec of a server. It should be either stored as a local file, or accessible by an URL. If you're not familiar with OpenAPI - it's a JSON or YAML file that lists server routes, expected parameters and responses. A lot of backend technologies support schema generation, for example [FastAPI](https://fastapi.tiangolo.com) has it out of the box. 

In this post we'll work with [Petstore](https://petstore3.swagger.io/) schema, so the first steps will be to install the library and generate types from OpenAPI schema (assuming you already have a SvelteKit application).

```bash
npm install @cocreators-ee/apity

npx openapi-typescript https://petstore3.swagger.io/api/v3/openapi.json --output src/petstore.ts
```

Now we have all the necessary information about the backend in `src/petstore.ts` - it contains all the routes and schemas for requests and responses. Using these objects, we can construct and API wrapper for the routes almost automatically:

```ts
import { Apity } from '@cocreators-ee/apity'
import type { paths } from 'src/petstore'

const apity = Apity.for<paths>()

apity.configure({
  baseUrl: 'https://petstore.swagger.io/v2',
})

export const findPetsByStatus = apity
  .path('/pet/findByStatus')
  .method('get')
  .create()
export const addPet = apity.path('/pet').method('post').create()
```

Yes, you have to define each API route by hand, but this was an intentional decision. You will operate with these functions in your code, so it's up to you how to name them. Assigning the names automatically from the spec could result in worse naming, or you might want to separate several parts into different wrappers.

Don't panic however, now TS knows your backend structure, so defining the routes will be rather quick, and it will warn you if you for example try to create a GET method for a POST route:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ifustupb40bikmlwqm37.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/612nsjjzwwcv1c09gnu8.png)

## Usage

Now when you have your desired routes defined, just import the functions and call them:

```ts
const request = findPetsByStatus({ status: 'available' })
const response = await request.result
```

Response object has a typed `data` property that's available when request is finished successfully. This can be checked with `ok` property:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b4aiu3ei0xeg2t7lmw4e.png)

This is crucial to check if request was successful before accessing the data, because failed request is also a valid case. This semantic forces you to handle bad cases as well, for example showing an error toast to the user.

Request parameters are also automatically suggested and validated for correctness:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8h75ceio5hxnim318p2c.gif)

Now that you've got a first impression of the library, let's dive into its features and see how it fits into different scenarios.

### Server Side Rendering (SSR)

You can use the library in SvelteKit's load functions, to request data on server:

```ts
import { findPetByStatus } from 'src/api.ts'

export async function load({ fetch }) {
  const request = findPetByStatus({ status: 'sold' }, fetch)
  const resp = await request.result
  if (resp.ok) {
    return { pets: resp.data, error: undefined }
  } else {
    return { pets: [], error: 'Failed to load pets' }
  }
}
```

Note that in this case you need to explicitly provide server's `fetch` implementation.

### Await syntax

If you like to await promises in Svelte templates, then you can use `ready` store from the request object:

```svelte
<script lang="ts">
  import { findPetByStatus } from 'src/api.ts'
  const request = findPetByStatus({ status: 'sold' })
  const petsReady = request.ready
</script>

<div>
  {#await $petsReady}
    <p>Loading..</p>
  {:then resp}
    {#if resp.ok}
      {#each resp.data as pet}
        <p>{pet.name}</p>
      {/each}
    {:else}
      <p>Error while loading pets</p>
    {/if}
  {/await}

  <button on:click={() => {request.reload()}}>
    Reload pets
  </button>
</div>
```

Autosuggestion works there as well!


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/56z5kw8ir71zzs1k1y6t.gif)


### Retrying a request

The curious reader has noticed that API call doesn't return a response directly, but a request object. It's designed to satisfy different needs:
- subscribe to a `ready` store and wait for updates
- await for response in template
- await the `result` promise right away (for example in server code)
- trigger a reload of the same request with the same parameters

For example you might want to periodically refresh the page content, and the parameters of such request stay the same:

```ts
import type { components } from 'src/petstore'

let pets: components['schemas']['Pet'][] = []
const request = findPetsByStatus({ status: 'available' })

request.resp.subscribe((resp) => {
    if (resp && resp.ok) {
        pets = resp.data
    }
})

setInterval(() => {
    request.reload()
}, 5000)
```

This example also shows that you can access backend types programmatically by importing them:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yi4p3gb155lynxrxcbsi.gif)

## Error handling

Errors can be expected and unexpected. First ones usually have an HTTP status code starting from 4, e.g 404 when a resource is not found on server, or 422 when your payload is incorrect. Sometimes you even have special handlers for them on frontend side. Often these requests have a JSON body in order to indicate what was wrong.
However under certain conditions a request can fail unexpectedly, for example when backend is down, or due to unstable network. These requests might not have a body, they usually throw an error on client side.

We thought that it's a good idea to provide a developer a single interface for handling both kinds of errors. Thus, each response has `ok`, `status` and `error` properties to debug the issue properly.
- `ok` is set to `false` for every failed request
- `status` is a HTTP status code and it's set by backend most of the times, but for exceptional cases like network errors it will be a negative value like `-1`
- `error` contains a JSON body from backend if provided or set to `undefined` otherwise.

Please always care about error cases to build better services!

## Bonus - pre-commit hooks

Now you have a mechanism to generate frontend types from OpenAPI spec, so backend becomes a single source of truth and you don't need to repeat a lot of code by hands. But there's still no answer on how to keep types in sync. One of the possible approaches is to use pre-commit hooks, like [pre-commit](https://pre-commit.com/) or [Husky](https://typicode.github.io/husky).

Pre commit hooks are set of commands to execute every time you run `git commit`. If you have a monorepo, then you can configure hooks to export OpenAPI spec and run `openapi-typescript` consequently. If your spec is accessible over network, then just create an npm script with a URL as a parameter. Possible example for Petsore:

`package.json`:

```json
{
  "devDependencies": {
    "openapi-typescript": "^6.2.0",
  },
  "scripts": {
    "make-api": "openapi-typescript https://petstore3.swagger.io/api/v3/openapi.json --output src/lib/openapi.ts"
  }
}
```

`.pre-commit-config.yaml`:

```yaml
repos:
  - repo: local
    hooks:
      - id: make-api
        name: make-api
        entry: npm run make-api
        language: system
        pass_filenames: false

```



## Conclusion

We hope that managing your API requests will be easier now thanks to Apity. We already use it in our SvelteKit projects and the experience is rather fun so far. Please try it out if you find this approach interesting enough, and your [feedback](https://github.com/cocreators-ee/apity/issues) is always appreciated.

Also I built a simple demo application using same Petstore spec, you can check it out at https://apity-demo.vercel.app.
Each page has a source code snippet for your convenience.

Thanks for reading!