Build a full-stack typescript nuxt application without the boilerplate code of tRPC using the nuxt-remote-fn module.
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.
Project Setup
Install the required packages.
pnpm add nuxt-remote-fn better-sqlite3
pnpm add -D @types/better-sqlite3
Configure the module by updating nuxt.config.ts
// 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
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.
<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.
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
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
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.
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 theimportant
flag in the database using the refimportantInput
- we added a button for saving the data and added a function
addStuff
as the click event handler.
<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 functionaddStuff
- added function
addStuff
, the function is using the imported methodaddTheStuff
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 refnewRecordResult
to the response from the server.
<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.
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.
<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 touseAsyncData
so that whenever our functionaddStuff
has a valid response we setnewRecordResult
and it will trigger theuseAsyncData
function to refetch the data
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.
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
<p v-if="newRecordError">Error Adding Record: {{ newRecordError }}</p>
Script Changes
- wrap the code in function with
try/catch
block and set the refnewRecordError
with the error message to be displayed in the template.
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