Getting started with Nuxt Server Handlers

Getting started with Nuxt Server Handlers

·

11 min read

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:

  • Basic understanding of TypeScript and Nuxt
  • Xata CLI installed
  • Xata account

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

running application

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.

create table and add column

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

copy database URL create and copy 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 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:

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

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

running application

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: