Nuxt, Next.js, SvelteKit, and others consistently innovate their solutions based on the server-side rendering paradigm. This paradigm generates web content on the server side for each request, leading to improved web application performance, SEO, and user experience.
Beyond simply outputting and generating content on the web page, a notable addition in the Nuxt release is the support for Server Handlers. This feature enables us to define functions that run securely on the server and can return JSON data, a promise, or use event.node.res.end()
as a response. The corresponding APIs can be called from Nuxt pages and components.
In this post, we will learn how to use Nuxt’s Server Handlers to create a basic todo application using the Xata serverless database platform. The project repository can be found here.
Prerequisites
To follow along in this tutorial, the following are required:
Project setup
In this project, we'll use a prebuilt UI to expedite development. To get started, let’s clone the project by navigating to a desired directory and running the command below:
git clone https://github.com/Mr-Malomz/server-handlers.git && cd server-handlers
Running the project
Next, we’ll need to install the project dependencies by running the command below:
npm i
Then, run the application:
npm run dev
Setup the database on Xata
To get started, log into the Xata workspace and create a todo
database. Inside the todo
database, create a Todo
table and add a description
column of type String
.
Get the Database URL and set up the API Key
To securely connect to the database, Xata provides a unique and secure URL for accessing it. To get the database URL, click the Get code snippet button and copy the URL. Then click the API Key link, add a new key, save and copy the API key.
Setup environment variable
To do this, update the nuxt.config.ts
file to define a runtime configuration the application will use to load environment variables.
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
css: ['~/assets/css/main.css'],
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
//add below
runtimeConfig: {
public: {
xataApiKey: '',
xataDatabaseUrl: '',
},
},
});
Next, we must create an index.d.ts
file in the root directory to type our runtime configuration.
declare module 'nuxt/schema' {
interface PublicRuntimeConfig {
xataDatabaseUrl: string;
xataApiKey: string;
}
}
export {};
Finally, we must add our database URL and API key as an environment variable. To do this, create .env
file in the root directory and add the copied URL and API key.
XATA_DATABASE_URL= <REPLACE WITH THE COPIED DATABASE URL>
XATA_API_KEY=<REPLACE WITH THE COPIED API KEY>
Integrate Xata with Nuxt
To seamlessly integrate Xata with Nuxt, Xata provides a CLI to install the required dependency and generate a fully type-safe API client. To do this, we need to run the command below:
xata init
On running the command, we’ll have to answer a few questions. Answer them as shown below:
Generate code and types from your Xata database <TypeScript>
Choose the output path for the generated code lib/xata.ts
With that, we should see a lib/xata.ts
file in the root directory.
A best practice is not to modify the generated code but to create a helper function to use it. To do this, create a utils/xataClient.ts
file in the root directory and insert the snippet below:
import { XataClient } from '~/lib/xata';
export const xataClient = () => {
const config = useRuntimeConfig();
const xata = new XataClient({
databaseURL: config.public.xataDatabaseUrl,
apiKey: config.public.xataApiKey,
branch: 'main',
});
return xata;
};
export interface ApiResponse<T> {
status: number;
message: string;
data?: T;
error?: {
message: string;
};
}
The snippet above imports the XataClient
class from the generated code and configures the client with the required parameters. In addition, we also define an ApiResponse
interface to describe our authentication response type.
Building the todo application
When a new Nuxt project is created, it includes a server
directory. This directory is meant for registering Server Handlers for our application. The process involves creating directories and files inside the server directory. Nuxt will automatically scan and register these files and directories as Server Handlers with support for Hot Module Replacement.
-| server/
---| api/
-----| hello.ts # /api/hello
---| routes/
-----| bonjour.ts # /bonjour
In our todo applications, we will use Server Handlers to do the following:
- Create a todo
- Get a todo
- Update a todo
- Delete a todo
- List todos
Create a todo
To create a todo, we need to create an api/createTodo.ts
inside the server
folder and insert the snippet below:
import { TodoRecord } from '~/lib/xata';
import { ApiResponse, xataClient } from '~/utils/xataClient';
export default defineEventHandler(async (event) => {
const xata = xataClient();
const { description } = await readBody(event);
const response = await xata.db.Todo.create({ description });
if (response.description) {
const successResponse: ApiResponse<TodoRecord> = {
status: 201,
message: 'success',
data: response,
};
return successResponse;
} else {
const failureResponse: ApiResponse<string> = {
status: 500,
message: 'failed',
error: {
message: 'Error creating todo',
},
};
return failureResponse;
}
});
The snippet above does the following:
- Imports the required dependencies
- Creates a handler that extracts the required information and uses the
xataClient
to create a todo. The function also uses theApiResponse
interface to return the appropriate response
Get a todo
To get a todo, we need to create a dynamic route api/[id].ts
file and insert the snippet below:
import { TodoRecord } from '~/lib/xata';
import { ApiResponse, xataClient } from '~/utils/xataClient';
export default defineEventHandler(async (event) => {
const xata = xataClient();
const id = event.context.params!.id;
if (!id) {
const emptyDescriptionResponse: ApiResponse<string> = {
status: 400,
message: 'failed',
error: {
message: 'No id provided.',
},
};
return emptyDescriptionResponse;
}
const response = await xata.db.Todo.read(id);
if (response) {
const successResponse: ApiResponse<TodoRecord> = {
status: 200,
message: 'success',
data: response,
};
return successResponse;
} else {
const failureResponse: ApiResponse<string> = {
status: 500,
message: 'failed',
error: {
message: 'Error getting todo',
},
};
return failureResponse;
}
});
The snippet above retrieves the dynamic parameter, checks if it is available, uses it to get the details of the associated todo, and returns the appropriate response.
Update a todo
To update a todo, we need to create an api/updateTodo.ts
file and insert the snippet below:
import { TodoRecord } from '~/lib/xata';
import { ApiResponse, xataClient } from '~/utils/xataClient';
export default defineEventHandler(async (event) => {
const xata = xataClient();
const { description, id } = await readBody(event);
const response = await xata.db.Todo.update(id, { description });
if (response) {
const successResponse: ApiResponse<TodoRecord> = {
status: 200,
message: 'success',
data: response,
};
return successResponse;
} else {
const failureResponse: ApiResponse<string> = {
status: 500,
message: 'failed',
error: {
message: 'Error updating todo',
},
};
return failureResponse;
}
});
The snippet above performs an action similar to the create todo functionality but updates the todo by searching for the corresponding todo and updating it.
Delete a todo
To delete a todo, we need to create an api/deleteTodo.ts
file and insert the snippet below:
import { ApiResponse, xataClient } from '~/utils/xataClient';
export default defineEventHandler(async (event) => {
const xata = xataClient();
const { id } = await readBody(event);
const response = await xata.db.Todo.delete(id);
if (response) {
const successResponse: ApiResponse<string> = {
status: 200,
message: 'success',
data: 'Todo deleted successfully',
};
return successResponse;
} else {
const failureResponse: ApiResponse<string> = {
status: 500,
message: 'failed',
error: {
message: 'Error deleting todo',
},
};
return failureResponse;
}
});
The snippet above gets the id
of a todo and uses the xataClient
to delete the matching todo.
List todos
To get the list of todos, we need to create an api/listTodo.ts
file and insert the snippet below:
import { TodoRecord } from '~/lib/xata';
import { ApiResponse, xataClient } from '~/utils/xataClient';
export default defineEventHandler(async (event) => {
const xata = xataClient();
const response = await xata.db.Todo.getAll();
if (response) {
const successResponse: ApiResponse<TodoRecord[]> = {
status: 200,
message: 'success',
data: response,
};
return successResponse;
} else {
const failureResponse: ApiResponse<string> = {
status: 500,
message: 'failed',
error: {
message: 'Error getting todo list',
},
};
return failureResponse;
}
});
The snippet above uses the xataClient
to get the list of todos and returns the appropriate responses.
Putting it all together!
With that done, we can start using the handlers in the UI.
Update the create todo component
To do this, we need to modify the components/TodoForm.vue
file as shown below:
<script setup lang="ts">
const description = ref<string>("");
const errorMsg = ref<string>("");
const emit = defineEmits();
const onSubmit = async () => {
const response = await $fetch("/api/createTodo", {
method: "POST",
body: { description: description.value }
})
if (response.status === 201) {
emit("todo-created", response.data);
description.value = "";
errorMsg.value = "";
} else {
errorMsg.value = String(response.error?.message)
}
}
</script>
<template>
<form @submit.prevent="onSubmit">
<p class="text-sm text-red-500 text-center" v-if="errorMsg !== ''">{{ errorMsg }}</p>
<textarea name="description" cols={30} rows={2} class="w-full border rounded-lg mb-2 p-4"
placeholder="Input todo details" required v-model="description" />
<div class="flex justify-end">
<div>
<button class="py-1 px-4 w-full h-10 rounded-lg text-white bg-zinc-800 hover:bg-zinc-900">Create</button>
</div>
</div>
</form>
</template>
The snippet above utilizes the createTodo
Server Handler by accessing it through the /api/createTodo
route to create a todo.
Update the edit todo component
To update a todo, first, we need to modify the components/EditTodoForm.vue
file as shown below:
<script setup lang="ts">
import type { TodoRecord } from '~/lib/xata';
const props = defineProps<{
todo: TodoRecord
}>();
const description = ref<string>("");
const errorMsg = ref<string>("");
watchEffect(() => {
if (props.todo) {
description.value = props.todo.description || "";
}
});
const onSubmit = async () => {
const response = await $fetch("/api/updateTodo", {
method: "PUT",
body: { id: props.todo.id, description: description.value }
})
if (response.status === 200) {
description.value = "";
errorMsg.value = "";
await navigateTo('/')
} else {
errorMsg.value = String(response.error?.message)
}
}
</script>
<template>
<form @submit.prevent="onSubmit">
<p class="text-sm text-red-500 text-center" v-if="errorMsg !== ''">{{ errorMsg }}</p>
<textarea name="description" cols={30} rows={2} className="w-full border rounded-lg mb-2 p-4"
placeholder="Input todo details" required v-model="description" />
<div className="flex justify-end">
<div>
<button class="py-1 px-4 w-full h-10 rounded-lg text-white bg-zinc-800 hover:bg-zinc-900">Update</button>
</div>
</div>
</form>
</template>
The snippet above does the following:
- Imports the required dependency
- Modify the component to accept a
todo
prop - Creates an onSubmit function that uses the
updateTodo
Server Handler by accessing it through the/api/updateTodo
route to update a todo
Lastly, we must modify the pages/[todo]/[id].vue
file to get the value of a matching todo and pass in the required prop to the EditTodoForm
component.
<script setup lang="ts">
import { X } from 'lucide-vue-next';
import type { TodoRecord } from '~/lib/xata';
const route = useRoute();
const todo = ref<TodoRecord>();
const errorMsg = ref<string>("");
const fetchData = async () => {
try {
const response = await $fetch<ApiResponse<TodoRecord>>(`/api/${route.params.id}`, {
method: "GET",
});
if (response.status === 200) {
todo.value = response.data;
} else {
errorMsg.value = response.error!.message;
}
} catch (error) {
errorMsg.value = "Error fetching data";
}
};
onMounted(() => {
fetchData();
});
</script>
<template>
<div class="relative z-10">
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div
class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<NuxtLink to="/" class="flex justify-end mb-2">
<X class="cursor-pointer" />
</NuxtLink>
<edit-todo-form :todo="todo!" />
</div>
</div>
</div>
</div>
</div>
</template>
Update the homepage to get the list of todos and delete a todo
To do this, we first need to modify the components/TodoComp.vue
file as shown below:
<script setup lang="ts">
import { Pencil, Trash2 } from 'lucide-vue-next';
import type { TodoRecord } from '~/lib/xata'; //add
const router = useRouter()
const props = defineProps<{
todos: TodoRecord[]
}>();
const errorMsg = ref<string>("");
const onDelete = async (id: string) => {
const response = await $fetch("/api/deleteTodo", {
method: "DELETE",
body: { id }
})
if (response.status === 200) {
router.go(0)
} else {
errorMsg.value = String(response.error?.message)
}
}
</script>
<template>
<div class='flex border p-2 rounded-lg mb-2' v-for="todo in props.todos" :key="todo.id">
<div class='ml-4'>
<header class='flex items-center mb-2'>
<h5 class='font-medium'>Todo item {{ todo.id }}</h5>
<p class='mx-1 font-light'>|</p>
<p class='text-sm'>{{ todo.xata.createdAt.toString().slice(0, 10) }}</p>
</header>
<p class='text-sm text-zinc-500 mb-2'>
{{ todo.description }}
</p>
<div class='flex gap-4 items-center'>
<NuxtLink :to="`todo/${todo.id}`" class='flex items-center border py-1 px-2 rounded-lg hover:bg-zinc-300'>
<Pencil class='h-4 w-4' />
<p class='ml-2 text-sm'>Edit</p>
</NuxtLink>
<button @click="onDelete(todo.id)" class='flex items-center border py-1 px-2 rounded-lg hover:bg-red-300'>
<Trash2 class='h-4 w-4' />
<p class='ml-2 text-sm'>
Delete
</p>
</button>
</div>
</div>
</div>
</template>
The snippet above does the following:
- Imports the required dependency
- Modify the component to accept a
todos
prop - Creates an
onDelete
function that uses thedeleteTodo
Server Handler by accessing it through the/api/deleteTodo
route to delete a todo - Uses the prop to loop and display the required information
Lastly, we need to update the pages/index.vue
file as shown below:
<script setup lang="ts">
import type { TodoRecord } from '~/lib/xata';
const todos = ref<TodoRecord[]>([]);
const errorMsg = ref<string>("");
const fetchData = async () => {
try {
const response = await $fetch<ApiResponse<TodoRecord[]>>("/api/listTodo", {
method: "GET",
});
if (response.status === 200) {
todos.value = response.data!;
} else {
errorMsg.value = response.error!.message;
}
} catch (error) {
errorMsg.value = "Error fetching data";
}
};
const handleTodoCreated = (createdTodo: TodoRecord) => {
todos.value.push(createdTodo);
};
onMounted(() => {
fetchData();
});
</script>
<template>
<main class="min-h-screen w-full bg-[#fafafa]">
<nav-bar />
<div class="w-full mt-6 flex justify-center">
<div class="w-full lg:w-1/2">
<todo-form @todo-created="handleTodoCreated" />
<section class="border-t border-t-zinc-200 mt-6 px-2 py-4">
<p class="text-sm text-red-500 text-center" v-if="errorMsg !== ''">{{ errorMsg }}</p>
<p className='text-sm text-zinc-500 text-center' v-else-if="todos.length === 0">No todos yet!</p>
<todo-comp v-else :todos="todos" />
</section>
</div>
</div>
</main>
</template>
The snippet above gets the list of todos and passes in the required props to the components.
With that done, we can test our application by running the following command:
npm run dev
Check out the demo below:
Conclusion
This post discusses how to use Nuxt Server Handlers to create a basic todo application. The server directory allows users to create APIs that run securely on the server.
Check out these resources to learn more: