Getting started with Nuxt Server Handlers
Tags: nuxt, serverless, fullstack, vue
Published: 2024-01-04 08:53:51

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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](https://github.com/Mr-Malomz/server-handlers/tree/dev).

## Prerequisites

To follow along in this tutorial, the following are required:

-   Basic understanding of TypeScript and Nuxt
-   Xata [CLI](https://xata.io/docs/getting-started/cli?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog) installed
-   Xata [account](https://app.xata.io/signin?mode=signup?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)

## 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:

```bash
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:

```bash
npm i
```

Then, run the application:

```bash
npm run dev
```

![running application](https://paper-attachments.dropboxusercontent.com/s_154D2F74346663ECD9524FF7F003EF6BE1F6D039235043BCBC915E2B1DB310D7_1703851199301_Screenshot+2023-12-29+at+12.59.50.png)

## Setup the database on Xata

To get started, log into the [Xata workspace](https://app.xata.io/workspaces) and create a `todo` database. Inside the `todo` database, create a `Todo` table and add a `description` column of type `String`.

![create table and add column](https://paper-attachments.dropboxusercontent.com/s_724B5DB0ED02FBEDE105C37DC2102A0D29EC66EAFDCC27146EA9AEF451272040_1703592508608_Screenshot+2023-12-26+at+13.07.33.png)

**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.

![click Get code snippet](https://paper-attachments.dropboxusercontent.com/s_154D2F74346663ECD9524FF7F003EF6BE1F6D039235043BCBC915E2B1DB310D7_1703842037934_Screenshot+2023-12-29+at+10.26.45.png)

![copy database URL](https://paper-attachments.dropboxusercontent.com/s_724B5DB0ED02FBEDE105C37DC2102A0D29EC66EAFDCC27146EA9AEF451272040_1703596013324_Screenshot+2023-12-26+at+14.04.29.png)
![create and copy API key](https://paper-attachments.dropboxusercontent.com/s_724B5DB0ED02FBEDE105C37DC2102A0D29EC66EAFDCC27146EA9AEF451272040_1703596030165_Screenshot+2023-12-21+at+23.42.03.png)

**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.

```ts
// 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.

```ts
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.

```bash
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](https://xata.io/docs/getting-started/cli) to install the required dependency and generate a fully type-safe API client. To do this, we need to run the command below:

```bash
xata init
```

On running the command, we’ll have to answer a few questions. Answer them as shown below:

```bash
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:

```ts
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](https://webpack.js.org/concepts/hot-module-replacement/).

```bash
    -| 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:

```ts
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 the `ApiResponse` 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:

```ts
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:

```ts
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:

```ts
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:

```ts
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:

```ts
<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:

```ts
<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.

```ts
<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:

```ts
<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 the `deleteTodo` 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:

```ts
<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:

```bash
npm run dev
```

Check out the demo below:

![running application](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExeXJyZzdzNGVwNGV0dm5vMnZyN25pajlheTUzZnBmZ2hxN2tjb2txNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/vamREBrckdxiADTtOY/giphy.gif)

## 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:

-   [Nuxt Server Handlers](https://nuxt.com/docs/guide/directory-structure/server?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)
-   [Xata documentation](https://xata.io/docs?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)