What is Xata?
Nuxt

Get started with Nuxt and Xata

Edit on GitHub

In this guide, you'll learn how to add Xata database and search functionality to a Nuxt application. You'll build the following basic blog application features:

  1. List all blog posts
  2. Retrieve and view a single blog post
  3. Full-text fuzzy search of blog posts

Although this application is a simple blog, you can apply these basics to other types of Nuxt applications.

The completed Nuxt and Xata code for this guide is available via the Xata examples repo on GitHub.

#

Before you begin

Install the Xata CLI:

npm install -g @xata.io/cli

Once installed, authenticate the Xata CLI with your Xata account. If you don't already have an account, you can use the same workflow to sign up for a new account. Run the following command to begin the authentication workflow:

xata auth login

On completion, the command will create a new API key for your user account, which you should see in the account settings page within the Xata UI. That key will also be stored locally on your computer (the location might vary for each OS). It looks like this:

# .config/xata/credentials
[default]
apiKey=YOUR_API_KEY_HERE

Begin by creating a new Nuxt application, accepting the default prompt options:

npx nuxi@latest init xata-nuxt

Once the command has completed, go to the xata-nuxt directory, install the dependencies, and run the application:

cd xata-nuxt
npm i
npm run dev

By default, the application will run on http://localhost:3000/.

Once you have the Xata CLI installed, are logged in, and have set up a new Nuxt application, you are ready to use the Xata CLI to generate a new database. Accept all the prompt defaults for the following command except for the region selection, where you should choose the region closest to your application users:

xata init

On completion, the CLI will create .env, .xatarc, and src/xata.ts files within your project folder with the correct credentials to access your database.

Your .env file should look something like this:

.env
XATA_API_KEY=YOUR_API_KEY_HERE
XATA_BRANCH=main

Since you selected TypeScript support after running xata init, it also created files that provide typings and functions to call using Xata's TypeScript SDK. This will additionally be referenced in the .xatarc file as follows:

{
  "databaseUrl": "https://my-xata-app-database-url",
  "codegen": {
    "output": "src/xata.ts"
  }
}

The src/xata.ts file includes generated code you should typically never touch manually.

You can use the Xata UI to manually define your schema and add data. However, for this guide, you'll use the Xata CLI and a CSV file to:

  1. Auto-generate a schema based on column headings for names and data types inferred from the column values
  2. Import data to the database

First, download the example blog posts CSV file. You can either do this manually or by running the following command:

curl --create-dirs -o seed/blog-posts.csv https://raw.githubusercontent.com/xataio/examples/main/seed/blog-posts.csv

Next, import the CSV:

xata import csv seed/blog-posts.csv --table Posts --create

Now, if you open up the Xata UI and navigate to your database, you will see the Posts table. Alternatively, you can run the command xata browse to open a browser window:

Posts table
Posts table

Click Schema to see the schema definition with the inferred data types:

Posts schema
Posts schema

You'll also see xata.* special columns automatically created and maintained by Xata.

With the database schema in place, the final step is to generate the code that allows you to access and query the data from our Nuxt application. To do this, run:

xata pull main

This updates the contents of src/xata.ts based on the schema defined on the main branch of your database. So, if you make any further changes to the schema, run xata pull <branch> to update the auto-generated code.

Begin by following step 2 onwards of the Tailwind CSS and Nuxt 3 integration guide. Once completed, add the following global styling to the newly created assets/css/main.css:

assets/css/main.css
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --foreground-rgb: 0, 0, 0;
  --background-start-rgb: 214, 219, 220;
  --background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
  :root {
    --foreground-rgb: 255, 255, 255;
    --background-start-rgb: 0, 0, 0;
    --background-end-rgb: 0, 0, 0;
  }
}

body {
  color: rgb(var(--foreground-rgb));
  background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
}

Finally, update app.vue to add some shared structure across application pages. The code will end up as follows:

app.vue
<script setup lang="ts">
useHead({
  title: 'Get started with Xata and Nuxt',
})
</script>
<template>
  <main class="flex flex-col items-center p-8 lg:p-24 min-h-screen">
    <div class="z-10 h-50 w-full max-w-5xl items-center justify-between text-xl lg:flex">
      <p class="fixed left-0 top-0 flex w-full justify-center pb-6 pt-8 lg:static lg:w-auto bg-gradient-to-b from-white via-white via-65% dark:from-black dark:via-black lg:bg-none">
        <a href="/">Get started with Xata and Nuxt</a>
      </p>
      <div class="fixed bottom-0 left-0 flex w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
        <a href="https://xata.io" class="w-20">
          <img src="https://raw.githubusercontent.com/xataio/examples/main/docs/app_logo.svg" />
        </a>
      </div>
    </div>
    <NuxtPage />
  </main>
</template>

Ensure the <NuxtPage /> component is before the closing </main> element.

You may need to restart your development server before all changes are detected.

The Nuxt app is now ready for integrating Xata into the codebase.

Start by making use of Nuxt's server routes to retrieve all the blog posts from Xata. Create server/api/posts.ts with the following contents:

server/api/posts.ts
import { getXataClient } from '../../src/xata';
const xata = getXataClient();

export default defineEventHandler(async (event) => {
  const posts = await xata.db.Posts.getAll();

  return posts;
});

First, import the getXataClient function and assign the result of calling that function to a variable named xata. This client uses the configuration set in the .env file.

Then, define and export a call to the defineEventHandler that returns a handler function. Within that handler function, use the xata client instance to get all the posts stored in the database. Active this via the auto-generated Posts property, which exposes a number of helper functions. In this case, use the getAll function to get all the Post records. Finally, return the posts as the function return value.

You can access http://localhost:3000/api/posts to see the posts represented as JSON.

Next, create a pages/index.vue file as a landing page to load the data and render the list of blog posts:

pages/index.vue
<script setup lang="ts">
const { data: posts } = await useFetch(`/api/posts`);
</script>

<template>
  <div class="w-full max-w-5xl mt-16">
    <p v-if="posts && posts.length === 0">No blog posts found</p>
    <div v-for="post in posts" class="mb-16">
      <p class="text-xs mb-2 text-purple-950 dark:text-purple-200">
        {{ new Date(post.pubDate || '').toDateString() }}
      </p>
      <h2 class="text-2xl mb-2">
        <a :href="`/posts/${post.slug}`">{{ post.title }}</a>
      </h2>
      <p class="text-purple-950 dark:text-purple-200 mb-5">{{ post.description }}</p>
      <a
        :href="`/posts/${post.slug}`"
        class="px-4 py-2 font-semibold text-sm bg-purple-700 text-white rounded-lg shadow-sm w-fit"
      >
        Read more &rarr;
      </a>
    </div>
  </div>
</template>

Here's a walkthrough of the code above.

First, fetch the data from the /api/posts endpoint and assign the result to a posts variable, making use of object destructuring and renaming data to posts.

Next, the UI is rendered. Check no records are present using Vue's v-if directive (v-if="posts && posts.length === 0"). If there are no posts, show a message saying, "No blog posts found". Otherwise, loop through the posts (v-for="post in posts") using Vue's v-for directive and access the columns of each Post record using their properties: pubDate to show the date the blog post was published, slug to link to individual blog posts (which will be used use later), title for the title of the post, and description for the textual description of the post.

You may need to restart your development server before all changes are detected.

This results in the page looking like the following:

List of blog posts
List of blog posts

You'll notice that the post heading and "Read more →" text use the slug property to link to a page that doesn't presently exist. That's the next step in this guide.

Create a new API endpoint to retrieve a single post by blog post via a new file, server/api/posts/[slug].ts:

server/api/posts/[slug].ts
import { getXataClient } from '../../../src/xata';
const xata = getXataClient();

export default defineEventHandler(async (event) => {
  const slug = getRouterParam(event, 'slug');
  const post = await xata.db.Posts.filter({ slug }).getFirst();

  return post;
});

Access the Xata client instance using the getXataClient auto-generated function as it is in server/api/posts.ts.

To handle the single posts identified by a slug, make use of Nuxt dynamic parameters that are defined and passed based on file and directory naming conventions.

Within the function wrapped in the call to defineEventHandler, the value of slug is retrieved using the getRouterParam, passing in the event and the name of the parameter to be retrieved. Then, use the Xata client instance filter function on the auto-generated Posts property to perform a query on the Posts table and find the record where the slug column equals the value of slug. Use the getFirst function to access the first (and only) Post result and return the post from the function.

You can test this endpoint out by accessing it with a slug value. For example, http://localhost:3000/api/posts/sql-mysql-postgresql-nosql.

Next, create a page to retrieve the post data from the new endpoint and render the contents of the single post to the UI. Create a new page, pages/posts/[slug].vue:

pages/posts/[slug].vue
<script setup lang="ts">
const route = useRoute();
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`);

useHead({ title: post?.title });
</script>

<template>
  <div class="w-full max-w-5xl mt-16">
    <p className="mb-2">
      <a href="/" class="text-purple-600"> &larr; Back to blog </a>
    </p>
    <h1 class="text-3xl mb-2">{{ post?.title }}</h1>
    <p class="text-sm mb-4 text-purple-950 dark:text-purple-200">
      {{ new Date(post?.pubDate || '').toDateString() }}
    </p>
    <p class="text-xl">{{ post?.description }}</p>
  </div>
</template>

As with the API route, page Routes in Nuxt support named route parameters represented in the file path. In this case. the parameter named slug. To get the value of the slug route parameter, get access to the route information using useRoute. Then, request the /api/posts/[slug] endpoint to fetch the single blog post information, passing the value of route.params.slug (await useFetch(`/api/posts/${route.params.slug}`)). Assign the resultant single Post information to a variable named post via object deconstruction and renaming (const { data: post }).

<script setup lang="ts">
const route = useRoute();
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`);
...

Next, use the value of post.title to set the page title using the useHead hook to set the page <title /> to match the blog post title:

...

useHead({ title: post?.title });
</script>

...

Finally, update the UI to show the title, pubDate, and description. Note that pubDate has been serialized, so has to be first converted back to a Date object in order to use the toDateString() function.

<template>
  <div class="w-full max-w-5xl mt-16">
    <p className="mb-2">
      <a href="/" class="text-purple-600"> &larr; Back to blog </a>
    </p>
    <h1 class="text-3xl mb-2">{{ post?.title }}</h1>
    <p class="text-sm mb-4 text-purple-950 dark:text-purple-200">
      {{ new Date(post?.pubDate || '').toDateString() }}
    </p>
    <p class="text-xl">{{ post?.description }}</p>
  </div>
</template>

The single blog post page will look as follows:

Single blog post
Single blog post

The last piece of functionality to add to the application is full-text fuzzy search of blog posts.

When you insert data into a Xata database, it is automatically indexed for full-text search. So you don't need to change any configuration to enable search, just need to use the TypeScript SDK search feature.

Aadd this functionality to /api/posts API server route:

server/api/posts.ts
import { getXataClient } from '../../src/xata';
const xata = getXataClient();

export default defineEventHandler(async (event) => {
  const query = getQuery(event);
  const search = query.q as string;

  let posts = null;
  if (search) {
    const { records } = await xata.db.Posts.search(search, { fuzziness: 2 });
    posts = records;
  } else {
    posts = await xata.db.Posts.getAll();
  }

  return posts;
});

Update the anonymous handler function to extract a query object from the event (const query = getQuery(event)) and get the value of the q querystring parameter, assigning it to a variable named search.

The landing page should list all blog posts if search is an empty string. However, if the search has a non-empty string value, a search is performed on the Posts table using the search function exposed on the auto-generated Posts property. Pass search as the text value to search for, and use a second options parameter with fuzziness set to 2, which informs the fuzzy search behavior to allow for two letters changed/added/removed. See fuzziness and typo tolerance for more details.

The last change enables the user to input and submit a search. Add a <form> to the page to allow a search value to be entered and submitted:

pages/index.vue
<script setup lang="ts">
const route = useRoute();
const search = route.query.q;
const { data: posts } = await useFetch(`/api/posts`, { query: { q: search } });
</script>

<template>
  <div class="w-full max-w-5xl mt-16">
    <form>
      <input
        name="q"
        v-model="search"
        placeholder="Search..."
        class="w-full rounded-lg p-2 dark:text-purple-950"
      />
    </form>
  </div>

  ...
</template>

useRoute is used similarly to the single blog post view, except this time you retrieve the value of the q querystring from route.query.q and assign it to a variable named search. The search value is then effectively proxied onto the /api/post API server route in the useFetch call where you add a second argument passing a query with a q querystring set to the value of search. As before, this performs the search against the Xata database and returns the results in the same structure.

The search value is used as the default value of the <input name="q" /> field to inform the user of the current search.

The default behavior of a form is to submit a GET request to the current URL with any form inputs added to the query string in the format {url}/?{input-name}={input-value}. For our search form, the result of a form submission is a GET request in the format?q={q-value}. With the browser default behavior set and the check for the q querystring search value already implemented, everything is in place.

Full-text fuzzy search
Full-text fuzzy search

The application now supports listing posts, viewing single posts via a slug page parameter, and full-text fuzzy search of posts.

In this guide, you've learned that Nuxt applications and Xata are a powerful combination. You created an application from scratch that lists blog posts, supports viewing a single blog post, and performs full-text fuzzy search on all posts.

You walked through setting up the Xata CLI and using it to:

  • Create a new Xata project
  • Create a database schema and populate it with data from an imported CSV file
  • Update the auto-generated code (in src/xata.ts) using xata pull main to reflect the updated schema

You then updated the landing page to list all blog posts, making use of the auto-generated xata.db.Posts.getAll function. You also added the single post page, making use of Nuxt page parameters where a slug was passed and used with xata.db.Posts.filter({ slug }).getFirst().

Finally, you added full-text fuzzy search functionality to the landing page, leveraging Xata's automatic table indexing. The search used a q query string and the auto-generated xata.db.Posts.search function.

If you enjoyed this guide, you could continue working on improving the application. Here are some suggestions:

  • Add pagination for the blog post listing
  • Add pagination for blog post search results
  • Handle single post view page not finding a result for a slug
  • Add a body field to the database schema to contain the full text of the blog post and update the single page view to use that new field

You can also explore some of the features covered in more detail:

Or dive into some of Xata's more advanced features, such as:

On this page

Before you beginCreate a new Nuxt appCreate a new databaseDefine the database schema and import CSV dataBasic styling and layoutQuery and list the postsQuery and show a single postsSearch postsWhat you've learnedLearn more