DEV Community

Cover image for Vue3 + TS + Vue Query + Express + tRPC: setup example
André Silva
André Silva

Posted on

Vue3 + TS + Vue Query + Express + tRPC: setup example

Table of contents

Introduction

Recently I was googling about trends in web dev to update myself on modern tools/libs/frameworks and I stumbled upon tRPC.

tRPC stands for TypeScript remote procedure call, and as you can read on its homepage, its purpose is to easily have End-to-end typesafe APIs. Essentially allows you to expose server functions that are callable from your client, your frontend, using all the goodies from TS.

Official tRPC website, a nice collection of examples and its docs.

tRPC is another way of ensuring a correct communication between client and server (via api calls). You might be already thinking about GraphQL to do so, but with tRPC you don't need to learn a new language, nor it is a schema. Whereas GraphQL is a schema and a language, that you use to detail the "shape" of the functions you can call from the server.

The experiment: Why not give it a shot using the latest Vue version, Vite, TypeScript and trying to plug in tRPC and see how it goes?
I tried to search for Vue based projects using tRPC and the vast majority of my hits were based on React/Next.js... So I decided to just start with a React based one and then experiment from that point on.

Notes:
- I will link all the relevant resources throughout the article
- This is just an experimental idea, to plug in several modern packages and create a very simplistic project
- This article is more towards people that have already some experience in web dev, however I'll try to provide some additional explanations

Setup

As a starting point I watched Jack Herrington's great video on "tRPC: Smart and Easy APIs", followed his steps and wondered how hard would it be to use Vue 3 and Vue Query, instead of React and React Query, respectively.

The next section shows how the final folder structure looks like, based on Jack's steps and after modifying it to use Vue.

Project folder structure

folder structure

It's a monorepo that uses yarn workspaces.
The server project is in the api-server folder and the frontend project is in the client folder.

Both server and client start up by running yarn start on the root dir, as you can see in the package.json in the root folder:
"start": "concurrently \"wsrun --parallel start\""

Server script

This is the server code, where we create our express app and tell it to use cors (to allow the calls from port 3000 to 8080) and also to use the trpcExpress middleware and register the router.

// packages\api-server\index.ts
import express from 'express';
import * as trpcExpress from '@trpc/server/adapters/express';
import { appRouter } from './router/app';
import cors from 'cors';

const main = async () => {
  const app = express();
  app.use(cors());
  const port = 8080;

  app.use(
    '/trpc',
    trpcExpress.createExpressMiddleware({
      router: appRouter,
      createContext: () => null,
    })
  );

  app.listen(port, () => {
    console.log(`api-server listening at http://localhost:${port}`);
  });
};

main();
Enter fullscreen mode Exit fullscreen mode

Router

The following code shows the router, which contains the access points:

  • 2 query endpoints (similar to a rest GET endpoint):
    • greetings
    • getMessages
  • 1 mutation endpoint (similar to a rest POST endpoint):
    • addMessage

Note: aside from adding data, a mutation can also update or delete data.

You can also see that I'm using zod, which is a "TypeScript-first schema declaration and validation library".

This package is going to be used to validate my inputs for queries/mutations (If needed, those validations can even throw validation messages).

z.string().uuid({ message: "Invalid UUID" });
Enter fullscreen mode Exit fullscreen mode

Note: And you can also use zod to infer types from zod objects, storing them as types and reusing them anywhere:

// packages\api-server\router\app.ts
import * as trpc from '@trpc/server';
import { z } from 'zod';
import { v4 as uuidv4 } from 'uuid';

export interface ChatMessage {
  id: string;
  user: string;
  message: string;
}

const messages: ChatMessage[] = [
  { id: uuidv4(), user: 'User1', message: 'This is my the first message!' },
  { id: uuidv4(), user: 'User2', message: 'Hello there 🎉' },
];

export const appRouter = trpc
  .router()
  .query('greetings', {
    resolve() {
      return {
        message: 'Greetings from /trpc/greetings:)',
      };
    },
  })
  .query('getMessages', {
    input: z.number().default(10),
    resolve({ input }) {
      return messages.slice(-input);
    },
  })
  .mutation('addMessage', {
    input: z.object({
      user: z.string(),
      message: z.string(),
    }),
    resolve({ input }) {
      const newMessage: ChatMessage = {
        id: uuidv4(),
        ...input,
      };
      messages.push(newMessage);
      return input;
    },
  });

export type AppRouter = typeof appRouter;
Enter fullscreen mode Exit fullscreen mode

The messages will be only stored in memory in this case, because I'm not using a DB to do that. (and makes it quicker to demo something).
It is also possible to create different routers which will contain different queries/mutations and then you can merge the routers to easily access a particular query from a router, on the client.

Vue Query initialization

This is how you initialize vue-query through VueQueryPlugin, in the main.ts file, which then gets used by the Vue application instance:

// packages\client\src\main.ts
import { createApp } from 'vue';
import { VueQueryPlugin } from 'vue-query';
import './style.css';
import App from './App.vue';

createApp(App).use(VueQueryPlugin).mount('#app');
Enter fullscreen mode Exit fullscreen mode

Why using Vue Query in the first place, you might ask?
"I could have done all the api calls using fetch/axios, right?"

True, however, this package offers neat features out of the box, such as caching, retry, refetch, infinite query (for infinite scroll), etc. Here are some challenges that might arise in your project with the increase of its complexity (Taken from the official docs):

  • Caching... (possibly the hardest thing to do in programming)
  • Deduping multiple requests for the same data into a single request
  • Updating "out of date" data in the background
  • Knowing when data is "out of date"
  • Reflecting updates to data as quickly as possible
  • Performance optimizations like pagination and lazy loading data
  • Managing memory and garbage collection of server state
  • Memoizing query results with structural sharing

And the hooks offer a set of standard props/functions for you to use in your app. Example of the useQuery hook:
Example of useQuery
Note: The data that you need to access is in the, conviniently named, data prop.

tRPC client

Here we are stating what is the url that we need to use from our tRPC client calls and also the types that we can use, coming from AppRouter. (Later on we will import this trpc const in the App.vue component):

// packages\client\src\api\trpc.ts
import { createTRPCClient } from '@trpc/client';
import { AppRouter } from 'api-server/router/app';

export const trpc = createTRPCClient<AppRouter>({
  url: 'http://localhost:8080/trpc',
});
Enter fullscreen mode Exit fullscreen mode

App component

For simplicity sake, this is the component where I decided to execute the tRPC client calls.
Note: I'm using Vue's script setup and having fun with it so far :)

<template>
  <div class="trpc-example">
    <h1>Vue 3 + vue-query + tRPC example</h1>
    <Error
      v-if="getMessagesHasError"
      error-message="Something went wrong - cannot fetch data"
      cta-text="Refetch data"
      @click="refetch()"
    />
    <Error
      v-if="addMessageHasError"
      error-message="Something went wrong - cannot submit message"
      cta-text="Reset error"
      @click="reset"
    />
    <div v-if="showFormAndMessages" class="trpc-example__container">
      <SendMessageForm :form="form" @submit-form="handleSubmitForm" />
      <h2 v-if="isLoading">Data is being loaded</h2>
      <Message v-for="chatMessage in data" :key="chatMessage.id" :chat-message="chatMessage" />
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, reactive } from 'vue';
import Message from './components/Message.vue';
import SendMessageForm from './components/SendMessageForm.vue';
import Error from './components/Error.vue';
import { useQuery, useMutation, useQueryClient } from 'vue-query';
import { trpc } from './api/trpc';
import { Form } from '../types';

const queryClient = useQueryClient();

const form = reactive({
  user: '',
  message: '',
});

const getMessages = () => trpc.query('getMessages');
const {
  isError: getMessagesHasError,
  isLoading,
  data,
  refetch,
} = useQuery('getMessages', getMessages, {
  refetchOnWindowFocus: false,
});

const addMessage = (form: Form) => trpc.mutation('addMessage', form);
const { error: addMessageHasError, mutate, reset } = useMutation('addMessage', addMessage);

const handleSubmitForm = () => {
  mutate(form, {
    onSuccess: () => {
      queryClient.invalidateQueries('getMessages');
    },
  });
};

const showFormAndMessages = computed(() => {
  return !getMessagesHasError.value && !addMessageHasError.value;
});
</script>
Enter fullscreen mode Exit fullscreen mode

App and examples

The best way to interact with this project is, obviously, by running it locally and see what you can do with it. But here are some examples:

This is how the client looks like (yes, I know, the UI looks fabulous!). The Vue.js devtools also displays information about the queries:
App UI and devtools

Data coming from /trpc/greetings:
greetings example data

Data coming from /trpc/getMessages:
getMessages example data

Examples of changing server side functions and observing TS safety checks on the client:
Ts safety 1Ts safety 2

You can also rename your server functions from the client (for some reason I was not able to rename the symbol from the server):
Ts safety

Example of blocking a query request and then calling the refetch function and its retries:
Refetch

Example of blocking a mutation request and then calling the reset function. This resets the error state:
Reset error

More useful links

I might create another repo to explore a more realistic project using Nuxt, tRPC, Vue Query, where I connect to a database and use the ORM Prisma, similarly to what Alex did in this pretty neat starter repo: https://github.com/trpc/examples-next-prisma-starter

Hope you found this article useful and that it allowed you to discover something today :)

Latest comments (5)

Collapse
 
eneskaplan profile image
Enes Kaplan

Hey André, quick question as I'm a newbie to tRPC and couldn't find information on this elsewhere through a quick look. Can tRPC used as a contract between frontend and backend, if the backend is written in another language like .NET or Java Springboot?

Collapse
 
alousilva profile image
André Silva

Hi Enes! (Sorry for the delayed reply!)

So as far as I understood tRPC is meant to be used in JS based frameworks/libraries on both server and client sides (hence the "t" in tRPC, for TypeScript) and enforce correct contracts on both sides.

For other languages such as .NET or Java you may look up gRPC.

Collapse
 
birimbau profile image
Tiago Neto

Excelent read

Collapse
 
alousilva profile image
André Silva

Glad you liked it 🙂

Some comments may only be visible to logged-in visitors. Sign in to view all comments.