Full Stack Nuxt Typescript App without tRPC
Tags: nuxt, vue, typescript, webdev
Published: 2023-09-13 20:36:22

Build a full-stack typescript nuxt application without the boilerplate code of tRPC using the [nuxt-remote-fn module](https://github.com/wobsoriano/nuxt-remote-fn).

We will walkthough the steps of building a simple Nuxt application to save and retrieve data from a SQLite database through a type-safe server-side API. 

This blog post is a companion to the video tutorial below.
{% youtube 37VR1hfHq1o %} 

##Project Setup
Install the required packages.

```bash
pnpm add nuxt-remote-fn better-sqlite3
pnpm add -D @types/better-sqlite3
```

Configure the module by updating `nuxt.config.ts`
```typescript
// nuxt.config.ts

export default defineNuxtConfig({
  modules: [
    'nuxt-remote-fn',
  ],
})
```
##Simple Implementation
Create a new file `api.server.ts` file in a new directory named `lib` directory.

Add a new function; this function will simply print out a string using the value passed in as the parameter
```typescript
export const basicFunction = (value: string) => {
  return `Basic function ${value}`;
};
```

You can see the function in action by updating the code in the `app.vue` file to the following.

Important thing to note is that you are simply importing the `api.server.ts` module but you get the function and the types associated with the function. You are making a server-side call without a lot of boilerplate, or traditional http request code.
```tsx
<template>
    <p>{{basicResult}}</p>
</template>
<script setup lang="ts">
import { basicFunction } from "./lib/api.server";

const basicResult = await basicFunction("Aaron");

</script>
```

That's basically the hello world of [nuxt-remote-fn module](https://github.com/wobsoriano/nuxt-remote-fn).


##Going Deeper
So for the next part we are going to create an api that allows us to read objects from sqlite database and write objects to the database.

The first step is creating a small object to set the database up and provide access to it.

create a file `/lib/sqlite-service`

```typescript
import Database from "better-sqlite3";

const db = new Database("stuff.db");
db.pragma("journal_mode = WAL");

const createTable = db.prepare(`
    CREATE TABLE IF NOT EXISTS stuff (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      stuff TEXT NOT NULL,
      important BOOL
    );
  `);

createTable.run();

export { db };
```
This sets up the database and creates the default table if it doesn't exist yet.

Now let's import the `sqlite-service` module into the `/lib/api.server.ts` file, and add a few types that we will be using
```typescript
import { db } from "./sqlite-service";

type Stuff = { id: number; stuff: string; important: 0 | 1 };
type StuffInput = { stuff: string; important: 0 | 1 };
``` 
### Add Stuff To Database

So let's add the function to add stuff to the database.
Notice how we are specifying the return type, so it is available in the client application and utilizing `db` from the sqlite-service.

We are also specifying the types for the function parameters using `StuffInput` type. This will be checked on client side and visible with intellisense.
```typescript
export const addTheStuff = async ({stuff, important}: StuffInput) => {

  // prepare the query
  const prep = db.prepare("INSERT INTO stuff (stuff, important)VALUES (?,? );");

  // execute the insert
  const result = prep.run(stuff, important);

  return result;
};
```

Next lets update the `app.vue` to add template changes for entering information to be saved to database and a simple function to insert items into the database.

**Template Changes**
- Updated the template to include an input field for getting the text description of the stuff, the field in bound to the ref `stuffInput`. We did the same for the `important` flag in the database using the ref `importantInput`
- we added a button for saving the data and added a function `addStuff` as the click event handler.

```tsx
<template>
  <div>
    <div style="margin: 16px">
      <div style="margin: 8px">
        <input type="text" placeholder="Describe Stuff" v-model="stuffInput" />
      </div>
      <div style="margin: 8px">
        <label for="important">
          <span>Important</span>
          <input
            v-model="importantInput"
            name="important"
            type="checkbox"
            placeholder="Important"
          />
        </label>
      </div>
      <button style="margin: 8px" @click="addStuff">ADD</button>
    </div>

  </div>
</template>
```


**Script Changes**
- imported `addTheStuff` function from `/lib/api.server`
- added refs for `stuffInput`, `importantInput`
- added ref for `newRecordResult` which will hold the result from the function `addStuff`
- added function `addStuff`, the function is using the imported method `addTheStuff` from `/lib/api.server`
- in the function we using the refs `stuffInput`, `importantInput` as parameters for saving the data. We do need to convert the boolean to an integer to be saved to the database and finally we clear the input refs and we set the ref `newRecordResult` to the response from the server.
```typescript
<script setup lang="ts">
import { addTheStuff } from "./lib/api.server";
const stuffInput = ref("");
const importantInput = ref<boolean>(false);
const newRecordResult = ref();

/**
 *
 */
const addStuff = async () => {
    newRecordResult.value = await addTheStuff({
      stuff: stuffInput.value,
      important: importantInput.value ? 1 : 0,
    });

    // clear input
    stuffInput.value = '';
    importantInput.value = false;
};
</script>
```
### Get All The Stuff Data

Now let's add the `getAllTheStuff` function to get all of the items from the database; notice how we are specifying the return type, so it is available in the client application and utilizing `db` from the sqlite-service.
```typescript
export const getAllTheStuff = async () => {
  // prepare the query
  const prep = db.prepare("SELECT * FROM stuff ");

  // execute the query
  const rows = prep.all() as Stuff[];

  return rows;
};
```

Next lets update the `app.vue` to add template changes for rendering the items in the database.

**Template Changes**
We wont need to be fancy, we will just loop through the values in the results from the server API query.

You can add this code to the bottom of the template. 
```tsx
<p v-if="error">Error: {{ error }}</p>
<div v-for="item in stuff" :key="item.id">
   <p>
    {{ item.important === 1 ? "[IMPORTANT] - " : '' }}
    {{item.stuff}}  
   </p>
</div>
```

**Script Changes**
The source code changes are pretty simple. 
- We need to import `getAllTheStuff` from `/lib/api.server.ts`
- We are using `useAsyncData` to query the database so we can just pass it our server function.
- We added the `watch` option to `useAsyncData` so that whenever our function `addStuff` has a valid response we set `newRecordResult` and it will trigger the `useAsyncData` function to refetch the data
```typescript
import { getAllTheStuff, addTheStuff } from "./lib/api.server";

const { data: stuff, error } = useAsyncData("stuff", () => getAllTheStuff(), {
  watch: [newRecordResult],
});
```

### Handling Errors
We can throw `NuxtError` from the remote server function that can then be handled by the client application.

In the code below we updated the `addStuff` remote server function to verify that there is a value for `stuff` and if now throw an error using `createError`, we also throw an error if the database cannot add the record.

```typescript
export const addTheStuff = async ({stuff, important}: StuffInput) => {

  if (!stuff  || stuff.length === 0) {
    throw createError('Invalid Parameter')
  }

  // prepare the query
  const prep = db.prepare("INSERT INTO stuff (stuff, important)VALUES (?,? );");

  // execute the insert
  const result = prep.run(stuff, important);

  if (result.changes === 0) {
    throw createError('Record Not Added')
  }

  return result;
};
```
Now that the errors are thrown, we can check on the client for the errors and display them in the UI.

**Template Changes**
```tsx
<p v-if="newRecordError">Error Adding Record: {{ newRecordError }}</p>
```

**Script Changes**
- wrap the code in function with `try/catch` block and set the ref `newRecordError` with the error message to be displayed in the template.

```typescript
const addStuff = async () => {
  try {
    newRecordResult.value = await addTheStuff({
      stuff: stuffInput.value,
      important: importantInput.value ? 1 : 0,
    });

    // clear input
    stuffInput.value = '';
    importantInput.value = false;

  } catch (error) {
    newRecordError.value = (error as NuxtError).data.message;
  }
};
```

###Links
- blog - https://dev.to/aaronksaunders/full-stack-nuxt-typescript-app-without-trpc-3io7
- nuxt-remote-fn - https://github.com/wobsoriano/nuxt-remote-fn
- better-sqlite-3 - https://github.com/WiseLibs/better-sqlite3

###Social Media 
Twitter - https://twitter.com/aaronksaunders
Facebook - https://www.facebook.com/ClearlyInnovativeInc
Instagram - https://www.instagram.com/aaronksaunders/
Dev.to - https://dev.to/aaronksaunders