Documentation
Creating custom DataService

Creating custom DataService

To create a custom DataService the easiest way is to implement our public IDataService interface.

Implementing the interface

export interface IDataService {
    getOne<T>(resource, params): Promise<T>
    getMany<T>(resource, params): Promise<T[]>
    getList<T>(resource, params): Promise<T[]>
    createOne<T>(resource, params): Promise<T>
    createMany<T>(resource, params): Promise<T[]>
    updateOne<T>(resource, params): Promise<T>
    updateMany<T>(resource, params): Promise<T[]>
    deleteOne<T>(resource, params): Promise<T>
    deleteMany<T>(resource, params): Promise<T[]>
}

The next example is showing how you would use browser fetch function to get the data from your API endpoint by some id through getOne method.

import { IDataService } from '@kickass-coderz/data-service'
 
class MyDataService implements IDataService {
    async getOne<T extends TBaseResponse>(
        resource: string,
        params: TGetOneParameters,
        context?: TQueryContext | undefined
    ): Promise<T> {
        const response = await fetch(`https://yourapi.com/api/${resource}/${params.id}`, {
            headers: {
                'content-type': 'application/json',
                accept: 'application/json'
            }
        })
        const result = await response.json()
 
        return result
    }
}

Even though DataService interface requires all of the CRUD methods, your API might not support all of those methods or you might not use them. The recommended way to handle this is to add a stub methods.

class MyDataService implements IDataService {
    // rest of the methods
 
    async createMany<T extends TBaseResponse>(resource: string, params: TCreateManyParameters): Promise<T[]> {
        throw new Error('Method not supported')
    }
 
    // rest of the methods
}

Like this you respect the interface requirements but you also catch any misuse inside your codebase. For example if anyone accidently uses this method it will throw an error and you can catch that during development or in production. Also when you wish to add support for some method you just replace the stub with a correct implementation.

As you can see it is competely your choice on what protocol or data fetching library you wish to use inside your DataService. We do however have a few tips you can also use to standardize your DataService and keep it more readable.

Using constructor for configuration

For this we will take an example from our RestDataService.

class RestDataService implements IDataService {
    constructor(baseUrl: string, fetchInstance?: typeof fetch) {
        this.baseUrl = baseUrl
 
        if (fetchInstance) {
            this.fetch = fetchInstance
        } else if (typeof window !== 'undefined') {
            this.fetch = window.fetch
        } else {
            throw new Error('fetch instance is required in non browser environments')
        }
    }
}

The constructor for our service expects baseUrl and fetchInstances parameters.

baseUrl would be a base of your API eg. https://yourapi.com/api. This is useful when you wish to use the same service for different APIs eg. our RestDataService is made to be compatible with any API following a REST specification. Also if you have different environments (development, staging, production) you can inject a different baseUrl depending on environment variable eg. from the .env file or during the build process.

fetchInstance is useful if you wish to use your service outside of the browser environemnt. For example during SSR (Server-Side Rendering). As Node does not have a fetch implementation by default you can inject node-fetch or any other preffered polyfill.

Reusable code

As DataService is just a class you can of course add any additional methods for the parts of the code you use across your data fetching methods.

For example in our RestDataService we add isValidId method which checks if the params.id is valid and it is called for any method that accepts this param.

class RestDataService implements IDataService {
    // rest of the methods
 
    private isValidId(value: string | number) {
        if (typeof value === 'number') {
            return true
        }
 
        if (typeof value === 'string') {
            // we do not want '/' character in our id param
            // to avoid unwanted any pathname traversal
            return !value.includes('/')
        }
 
        return false
    }
 
    async updateOne<T extends TBaseResponse>(resource: string, params: TUpdateOneParameters): Promise<T> {
        if (!this.isValidId(params.id)) {
            throw new Error('Invalid params.id')
        }
 
        // fetch the data
 
        return result
    }
 
    // rest of the methods
}

Also you can use private properties on the class. For example in our RestDataService we have property containing usual headers that are required for the REST APIs.

private readonly jsonHeaders = {
    'content-type': 'application/json',
    accept: 'application/json'
}

An then we can pass those to our fetch instance whenever we call it. You can see we also use baseUrl here (which is set through constructor).

const response = await this.fetch(`${this.baseUrl}/${resource}/${params.id}`, {
    headers: this.jsonHeaders
})

This is especially useful when you use some more advanced HTTP client packages like axios or ky-universal which give you even more options to instantiate them.

For more inspiration you can always take a look at a source code of our data services or ask on GitHub.

Plain object DataService

Even though we recomment using classes for implementing your DataService you can also create on with a plain object.

const dataService: IDataService = {
    async getOne<T extends TBaseResponse>(
        resource: string,
        params: TGetOneParameters,
        context?: TQueryContext | undefined
    ): Promise<T> {
        const response = await fetch(`https://yourapi.com/api/${resource}/${params.id}`, {
            headers: {
                'content-type': 'application/json',
                accept: 'application/json'
            }
        })
        const result = await response.json()
 
        return result
    }
 
    // rest of the methods
}