VitNode

Fetching Data

How to fetch data in VitNode.

VitNode provides a way to fetch data from the backend using the fetcher() function on Server Side Rendering (SSR) and fetcherClient() on Client Side Rendering (CSR).

Fetcher (SSR)

The fetcher() function use the next context from the server. That means you can use it only in React Server Components (RSC) or Server Function.

example.tsx
import { fetcher } from 'vitnode-frontend/api/fetcher';
import { ExampleWelcomeObj } from 'shared/welcome/example.dto';
 
const Example = () => {
  const { data } = await fetcher<ExampleWelcomeObj>({
    url: '/welcome/example',
  });
 
  return <div>{data.data}</div>;
};

The fetcher function is just a wrapper around the fetch function with default headers, error handles, default backend URL, and more.

Fetcher Client (CSR)

The fetcherClient() function is used on the client-side to fetch data from the backend.

example.tsx
'use client';
 
import { fetcherClient } from 'vitnode-frontend/api/fetcher-client';
import {
  ItemExampleWelcomeObj,
  EditExampleWelcomeBody,
} from 'shared/welcome/example.dto';
 
export const Example = () => {
  const onClick = async () => {
    await fetcherClient<ItemExampleWelcomeObj, EditExampleWelcomeBody>({
      url: '/welcome/example',
      method: 'PUT',
      body: {
        name: 'John Doe',
      },
    });
  };
 
  return (
    <button type="button" onClick={onClick}>
      Click
    </button>
  );
};

Files Upload

To upload files from the frontend, you can use the fetcherClient function with FormData.

example.tsx
const onClick = async () => {
  const formData = new FormData(); 
 
  // Handle files to upload
  formData.append('icon', file); 
 
  await fetcherClient<ItemExampleWelcomeObj, EditExampleWelcomeBody>({
    url: '/welcome/example',
    method: 'PUT',
    body: formData, 
  });
};

Type-Safe Fetcher

The fetcher() and fetcherClient() functions are type-safe. You can pass the DTO object to the function by generic type.

Response

The response object is required. As an example we use the ExampleWelcomeObj DTO object.

example.tsx
import { fetcher } from 'vitnode-frontend/api/fetcher';
import { ExampleWelcomeObj } from 'shared/welcome/example.dto'; 
 

const { data } = await fetcher<ExampleWelcomeObj>({
  url: '/welcome/example',
});

Body and Query

Our fetcher functions are not just type-safe for the body and query, they have also converter function to the correct format so you don't need to worry about "How to pass query into url?" or "How to pass body into fetcher using only string?".

example.tsx
import { fetcherClient } from "vitnode-frontend/api/fetcher-client";
import { ItemExampleWelcomeObj, EditExampleWelcomeBody } from "shared/welcome/example.dto"; 
 

const { data } = await fetcher<ExampleWelcomeObj, EditExampleWelcomeBody>({
  url: "/welcome/example"
  method: 'POST',

  body: {
    name: "John Doe"

  }
});

or for query:

example.tsx
import { fetcherClient } from "vitnode-frontend/api/fetcher-client";
import { ItemExampleWelcomeObj, EditExampleWelcomeQuery } from "shared/welcome/example.dto"; 
 

const { data } = await fetcher<ExampleWelcomeObj, EditExampleWelcomeQuery>({
  url: "/welcome/example"
  method: 'POST',

  query: {
    name: "John Doe"

  }
});

Param

We don't have a converter for the param, but you can pass it directly into the URL.

example.tsx
import { fetcher } from 'vitnode-frontend/api/fetcher';
import { ExampleWelcomeObj } from 'shared/welcome/example.dto';
 

const id = 1;
const { data } = await fetcher<ExampleWelcomeObj>({

  url: `/welcome/example/${id}`,
});

Revalidate data

NextJS allows you to use Revalidating data in Server Function.

Server Function

You can use the fetcher() function in the Server Function.

mutation-api.ts
'use server';
 
import { fetcher } from 'vitnode-frontend/api/fetcher';
import { revalidatePath } from 'next/cache';
import {
  CreatePluginsAdminBody,
  ShowPluginAdmin,
} from 'vitnode-shared/admin/plugins.dto';
 
export const mutationApi = async (body: CreatePluginsAdminBody) => {
  await fetcher<ShowPluginAdmin, CreatePluginsAdminBody>({
    url: '/admin/plugins',
    method: 'POST',
    body,
  });
 
  revalidatePath('/', 'layout');
};

Catch error

Using Server Function you can catch errors only on server side and pass them into client. As an example we will handle the PLUGIN_ALREADY_EXISTS error to show a message in the form and toast if the error is different.

mutation-api.ts
'use server';
 
import { fetcher } from '@/api/fetcher';
import { cookies } from 'next/headers';
import { CreateExampleBody } from 'shared/example/example.dto';
 
export const mutationApi = async (body: CreateExampleBody) => {
  try {
    await fetcher<object, CreateExampleBody>({
      method: 'POST',
      url: '/core/plugins',
      body,
    });
  } catch (err) {

    const { message } = err as Error;

    if (message.includes('PLUGIN_ALREADY_EXISTS')) {

      return { message: 'PLUGIN_ALREADY_EXISTS' };

    }
 

    return { message: 'INTERNAL_SERVER_ERROR' };
  }
};
use-handle-example-api.ts
import { fetcher } from 'vitnode-frontend/api/fetcher';
import { CreateExampleBody } from 'shared/example/example.dto';
import { mutationApi } from './mutation-api';
 
export const useHandleExampleApi = () => {
  const onSubmit = async (
    values: z.infer<typeof formSchema>,
    form: UseFormReturn<z.infer<typeof formSchema>>,
  ) => {
    const mutation = await mutationApi(body);

    if (!mutation.message) return; // If no error, return

    if (mutation.message === 'PLUGIN_ALREADY_EXISTS') {

      form.setError('code', {
        message: t('create.code.exists'), 

      });
 

      return;
    }
 

    toast.error(tCore('title'), {

      description: error.message,

    });
  };
 
  return { onSubmit };
};

Don't handle errors directly on the client side using try/catch!

NextJS will not show the error message on the client side if you use try/catch on production. In dev mode, it will show the error message but not in production.

Debounced fetching

To avoid fetching data on every key press you can use useDebouncedCallback from use-debounce package.

Here is an example how to use it for search input.

import { useDebouncedCallback } from 'use-debounce';
 
const handleSearchInput = useDebouncedCallback((value: string) => {
  setSearch(value);
}, 500);
 
return <Input onChange={e => handleSearchInput(e.target.value)} />;

On this page