Skip to content
/ data Public

Repository pattern implementation with a tiny HttpClient based on Fetch API.

License

Notifications You must be signed in to change notification settings

volverjs/data

Repository files navigation

volverjs

@volverjs/data

repository http-client url-builder fetch

Quality Gate Status Maintainability Rating Security Rating Depfu Depfu


maintained with ❤️ by


8 Wave


Install

# pnpm
pnpm add @volverjs/data

# yarn
yarn add @volverjs/data

# npm
npm install @volverjs/data --save

Usage

This library exports four main classes: Hash, UrlBuilder, HttpClient and RepositoryHttp.

import { Hash, HttpClient, RepositoryHttp, UrlBuilder } from '@volverjs/data'

Hash

The Hash class provides some static functions to generate hashes.

import { Hash } from '@volverjs/data'
const hash = Hash.cyrb53('hello world')

UrlBuilder

The UrlBuilder class provides a way to build URLs with template parameters and query string.

import { UrlBuilder } from '@volverjs/data'

const urlBuilder = new UrlBuilder({
    encodeValuesOnly: false
})
const url = urlBuilder.build('https://my.api.com/:endpoint', {
    endpoint: 'users',
    _limit: 10,
    _page: 1
})
// url = 'https://my.api.com/users?_limit=10&_page=1'

Instead of URLSearchParams, the query parameters are automatically encoded using qs library. Please refer to the UrlBuilder docs for more informations.

HttpClient

The HttpClient class is a wrapper around ky, a client based on fetch API . It provides a simple interface to make HTTP requests and uses UrlBuilder to build URLs.

import { HttpClient } from '@volverjs/data'

const client = new HttpClient({
    prefixUrl: 'https://my.api.com'
})
const response = await client.get({
    template: ':endpoint/:action?/:id',
    params: {
        endpoint: 'users',
        id: 1,
        _limit: 10,
        _page: 1
    }
})
// fetch('https://my.api.com/users/1?_limit=10&_page=1', { method: 'GET' })

Please refer to the HttpClient docs for more informations.

RepositoryHttp

The RepositoryHttp class is an implementation of the Repository interface for http requests using HttpClient. It was designed with the repository pattern in mind to provide a simple way to make CRUD operations on a REST API.

import { HttpClient, RepositoryHttp } from '@volverjs/data'

class User {
    id: number
    name: string
    surname: string
    constructor(data: { id: number, name: string, surname: string }) {
        this.id = data.id
        this.name = data.name
        this.email = data.email
    }

    get fullName() {
        return `${this.name} ${this.surname}`
    }
}

const client = new HttpClient({
    prefixUrl: 'https://my.api.com'
})

const repository = new RepositoryHttp<User>(client, 'users/:group?/:id?', {
    class: User
})

const getAdminUsers: User[] = async () => {
    const { responsePromise } = repository.read({
        group: 'admin'
    })
    const { data } = await responsePromise
    return data
}

Please refer to the RepositoryHttp docs for more informations.

Vue

You can use this library with Vue 3 with @volverjs/data/vue.

Plugin

The createHttpClient function returns a plugin that can be installed in a Vue app and has a property with the global httpClient instance: httpClientPlugin.globalInstance.

import { createHttpClient } from '@volverjs/data/vue'
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
const httpClientPlugin = createHttpClient({
    prefixUrl: 'https://my.api.com'
})

app.use(httpClientPlugin, {
    globalName: 'vvHttp' // (optional) default: 'vvHttp'
})

With app.use(httpClientPlugin) the HttpClient instance will be available in all components as $vvHttp with Options API. Use globalName to change the name vvHttp.

Composition API

Alternatively, you can use the useHttpClient() and useRepositoryHttp() composables to get the HttpClient instance in a specific component.

useHttpClient()

If HttpClientPlugin is not created with createHttpClient() or the httpClient scope requested not exist (ex: useHttpClient('instanceNotExist')), then useHttpClient() throw the error HttpClient instance not found.

<script lang="ts">
import { createHttpClient, useHttpClient } from '@volverjs/data/vue'
</script>

<script lang="ts" setup>
import { computed, ref } from 'vue'

createHttpClient({
    prefixUrl: 'https://my.api.com'
})

const { client } = useHttpClient()
const isLoading = ref(false)
const isError = computed(() => error.value !== undefined)
const error = ref()
const data = ref<Data>()

interface User {
    id: number
    name: string
}

async function execute() {
    isLoading.value = true
    error.value = undefined
    try {
        const response = await client.get('users/1')
        data.value = await response.json<User>()
    }
    catch (e) {
        error.value = e.message
    }
    finally {
        isLoading.value = false
    }
}
</script>

<template>
    <div>
        <button @click="execute()">
            Execute
        </button>
        <div v-if="isLoading">
            Loading...
        </div>
        <div v-if="isError">
            {{ error }}
        </div>
        <div v-if="data?.[0]">
            {{ data?.[0].name }}
        </div>
    </div>
</template>

useHttpClient() also exposes request(), requestGet(), requestPost(), requestPut(), requestPatch() and requestDelete() methods. These methods are wrappers around the HttpClient methods with reactivity.

<script lang="ts" setup>
import { useHttpClient } from '@volverjs/data/vue'

interface User {
    id: number
    name: string
}
const { requestGet } = useHttpClient()
const { isLoading, isError, isSuccess, error, data, execute }
    = requestGet<User>('users/1', {
        immediate: false
    })
</script>

<template>
    <div>
        <button @click="execute()">
            Execute
        </button>
        <div v-if="isLoading">
            Loading...
        </div>
        <div v-if="isError">
            {{ error }}
        </div>
        <div v-if="isSuccess">
            {{ data.name }}
        </div>
    </div>
</template>

Each method returns an object with the following properties:

  • isLoading: a computed that indicates if the request is loading;
  • isError: a computed that indicates if the request has failed;
  • isSuccess: a computed that indicates if the request has succeeded;
  • error: a readonly ref that contains the error message;
  • response: a ref that contains the response;
  • data: a ref that contains the response data (.json() function);
  • execute(): a function that executes the request.

The request can be executed later by setting the immediate option to false (default: true).

<script lang="ts" setup>
import { useHttpClient } from '@volverjs/data/vue'

interface User {
    id: number
    name: string
}

const data = ref<Partial<User>>({ name: '' })

const { requestPost } = useHttpClient()
const { isLoading, isError, isSuccess, error, execute } = requestPost<User>(
    'users',
    computed(() => ({ immediate: false, json: data.value }))
)
</script>

<template>
    <form @submit.prevent="execute()">
        <div v-if="isLoading">
            Loading...
        </div>
        <div v-if="isError">
            {{ error }}
        </div>
        <div v-if="isSuccess">
            Success!
        </div>
        <input v-model="data.name" type="text">
        <button type="submit">
            Submit
        </button>
    </form>
</template>

The execute() function returns an object with the following properties:

  • responsePromise: a Promise that resolves with the response;
  • abort: a function that aborts the request;
  • signal: an AbortSignal that can be used to check if the request has been aborted.

useRepositoryHttp()

To create a RepositoryHttp instance, you can use the useRepositoryHttp() composable.

Parameters
  • template: string | HttpClientUrlTemplate,
  • options?: RepositoryHttpOptions
Example 1
<script lang="ts" setup>
import { useRepositoryHttp } from '@volverjs/data/vue'
import { computed, ref } from 'vue'

interface User {
    id: number
    name: string
}

const { repository } = useRepositoryHttp<User>('users/:id')
const isLoading = ref(false)
const isError = computed(() => error.value !== undefined)
const error = ref()
const item = ref()

async function execute() {
    isLoading.value = true
    try {
        const { responsePromise } = repository.read({ id: 1 })
        const response = await responsePromise
        item.value = response.item
    }
    catch (e) {
        error.value = e.message
    }
    finally {
        isLoading.value = false
    }
}
</script>

<template>
    <div>
        <button @click="execute">
            Execute
        </button>
        <div v-if="isLoading">
            Loading...
        </div>
        <div v-if="isError">
            {{ error }}
        </div>
        <div v-if="item">
            {{ item.name }}
        </div>
    </div>
</template>
Example 2 - Typing server response for responseAdapter
<script lang="ts" setup>
import { useRepositoryHttp } from '@volverjs/data/vue'
import { computed, ref } from 'vue'

interface IUser {
    id: number
    name: string
}

interface UserResponse {
    id: number
    firtname: string
    lastname: string
}

class User implements IUser {
    id: number
    name: string

    constructor(data: UserResponse) {
        this.id = data.id
        this.name = `${data.firtname} ${data.lastname}`
    }
}

const { repository } = useRepositoryHttp<IUser, UserResponse>('users/:id', {
    responseAdapter: raw => [new User(raw)] // -----> raw is type of UserResponse instead of "unknown"
})
const isLoading = ref(false)
const isError = computed(() => error.value !== undefined)
const error = ref()
const item = ref()

async function execute() {
    isLoading.value = true
    try {
        const { responsePromise } = repository.read({ id: 1 })
        const response = await responsePromise
        item.value = response.item
    }
    catch (e) {
        error.value = e.message
    }
    finally {
        isLoading.value = false
    }
}
</script>

<template>
    <div>
        <button @click="execute">
            Execute
        </button>
        <div v-if="isLoading">
            Loading...
        </div>
        <div v-if="isError">
            {{ error }}
        </div>
        <div v-if="data">
            {{ data.name }}
        </div>
    </div>
</template>

useRepositoryHttp() also exposes create(), read(), update() and remove() methods. These methods are wrappers around the RepositoryHttp methods with reactivity.

<script lang="ts" setup>
import { useRepositoryHttp } from '@volverjs/data/vue'

interface User {
    id: number
    name: string
}

const { read } = useRepositoryHttp<User>('users/:id')
const { isLoading, isError, error, item, execute } = read(
    { id: 1 },
    { immediate: false }
)
</script>

<template>
    <div>
        <button @click="execute">
            Execute
        </button>
        <div v-if="isLoading">
            Loading...
        </div>
        <div v-if="isError">
            {{ error }}
        </div>
        <div v-if="item">
            {{ item.name }}
        </div>
    </div>
</template>

Each method returns an object with the following properties:

  • isLoading: a computed that indicates if the request is loading;
  • isSuccess: a computed that indicates if the request has succeeded;
  • isError: a computed that indicates if the request has failed;
  • error: a readonly ref that contains the error message;
  • execute(): a function that executes the request.

create(), read(), update() also return:

  • data a ref that contains the response data;
  • metadata a ref that contains the response metadata;

read() also returns:

  • item a ref that contains the first item of the response data;

The request can be executed later by setting the immediate option to false (default: true).

<script lang="ts" setup>
import { useRepositoryHttp } from '@volverjs/data/vue'
import { computed } from 'vue'

interface User {
    id: number
    name: string
}

const { read, update } = useRepositoryHttp<User>('users/:id?')
const { isLoading: isReading, error: readError, item } = read({ id: 1 })
const {
    isLoading: isUpdating,
    error: updateError,
    execute
} = update(item, { id: 1 }, { immediate: false })

const isLoading = computed(() => isReading.value || isUpdating.value)
const error = computed(() => updateError.value || readError.value)
</script>

<template>
    <form @submit.prevent="execute()">
        <div v-if="isLoading">
            Loading...
        </div>
        <div v-if="error">
            {{ error }}
        </div>
        <template v-if="item">
            <input v-model="item.name" type="text">
            <button :disabled="isLoading" type="submit">
                Submit
            </button>
        </template>
    </form>
</template>

The execute() function returns an object with the following properties:

  • responsePromise: a Promise that resolves with the response;
  • abort: a function that aborts the request;
  • signal: an AbortSignal that can be used to check if the request has been aborted.

Advanced usage

HttpClientPlugin manage most of use cases (ex: micro-frontend with different httpClient, a SPA with authenticated API calls and public API calls, etc..). The HttpClientPlugin can manage a Map of httpClient instances.

createHttpClient( ) with scope parameter

With scope parameter on createHttpClient() multiple httpClient instances can be created. If the httpClient scope instance already exist an error is throwed: httpClient with scope ${scope} already exist.

Parameters:

options?: HttpClientInstanceOptions & { scope: string }

Example:
<script lang="ts" setup>
import { createHttpClient } from '@volverjs/data/vue'

createHttpClient({ scope: 'v2Api', prefixUrl: 'https://my.api.com/v2' })

const { requestGet } = useHttpClient('v2Api')

const { isLoading, isError, data } = requestGet<User>('users')
</script>

removeHttpClient( )

With this composable the httpClient instance can be removed from Map instances. The global httpClient instance cannot be removed.

Parameters:

scope: string,

Example:
<script lang="ts" setup>
import { addHttpClient, removeHttpClient } from '@volverjs/data/vue'

createHttpClient('v2Api', { prefixUrl: 'https://my.api.com/v2' })

const { requestGet } = useHttpClient('v2Api')

const { isLoading, isError, data } = requestGet<User>('users')

removeHttpClient('v2Api')
</script>

Note: The httpClient Map instances is NOT reactive, so after the removeHttpClient, the httpClient used before will NOT be destroyed.

Acknoledgements

The UrlBuilder class is inspired by urlcat.

License

MIT