What is Xata?
Next.js

Get started with Next.js and Xata

Edit on GitHub

In this guide, you'll create a new Next.js application and walk through adding Xata database and search functionality. 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 Next.js applications.

The completed Next.js 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

Once completed, the command will create a new API key for your user account, which you should see in the "Account Settings" page in the Xata UI. That key will also be stored locally on your computer (the location might vary for depending on your operating system). It looks like this:

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

Begin by creating a new Next.js application:

npx create-next-app@latest --typescript xata-nextjs

Once the command has completed, go to the xata-nextjs directory and run the application:

cd xata-nextjs
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 Next JS 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, it also created files that provide typings and functions to call using Xata's TypeScript SDK. This will also 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 manually configure.

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 Next.js 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 our database. If you make any further changes to the schema, run xata pull <branch> to update the auto-generated code.

Update src/app/layout.tsx to contain the following code to add some basic common structure to the application:

src/app/layout.tsx
import './globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'Get started with Xata and Next.js'
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <main className="flex flex-col items-center p-8 lg:p-24 min-h-screen">
          <div className="z-10 h-50 w-full max-w-5xl items-center justify-between text-xl lg:flex">
            <p className="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 Next.js</a>
            </p>
            <div className="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" className="w-20">
                <img src="https://raw.githubusercontent.com/xataio/examples/main/docs/app_logo.svg" />
              </a>
            </div>
          </div>
          {children}
        </main>
      </body>
    </html>
  );
}

Ensure the {children} are rendered before the closing </main> element.

Now, you're ready to integrate Xata into the Next.js codebase. Let's start by stripping back the landing page, src/app/page.tsx, to a bare template:

src/app/page.tsx
export default async function Home() {
  return (
    <>
      <div className="w-full max-w-5xl mt-16">No posts</div>
    </>
  );
}

Next, import the auto-generated getXataClient function from src/xata.ts, get all the posts using the client, and list them within the page:

src/app/page.tsx
import { getXataClient } from '@/xata';

const xata = getXataClient();

export default async function Home() {
  const posts = await xata.db.Posts.getAll();

  return (
    <>
      <div className="w-full max-w-5xl mt-16">
        {posts.length === 0 && <p>No blog posts found</p>}
        {posts.map((post) => (
          <div key={post.id} className="mb-16">
            <p className="text-xs mb-2 text-purple-950 dark:text-purple-200">{post.pubDate?.toDateString()}</p>
            <h2 className="text-2xl mb-2">
              <a href={`posts/${post.slug}`}>{post.title}</a>
            </h2>
            <p className="text-purple-950 dark:text-purple-200 mb-5">{post.description}</p>
            <a
              href={`posts/${post.slug}`}
              className="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>
    </>
  );
}

Let's break down what's happening in the code above.

First, import the getXataClient function and assign the result of calling that function to a variable named xata:

import { getXataClient } from '@/xata';

const xata = getXataClient();

Then, in the Home function, use the xata client instance to get all the posts stored in the database. You achieve 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.

export default async function Home() {
  const posts = await xata.db.Posts.getAll();

  ...
}

Finally, update the UI to display the result of the getAll call. If no Post records are present (posts.length === 0), the message "No blog posts found" is displayed. Otherwise, loop through the posts using posts.map and access the columns of each Post record using their properties: id as a unique identifier for the key attribute, pubDate to show the date the blog post was published, slug to link to individual blog posts (which you'll use later), title for the title of the post, and description for the textual description of the post:

<div className="w-full max-w-5xl mt-16">
  {posts.length === 0 && <p>No blog posts found</p>}
  {posts.map((post) => (
    <div key={post.id} className="mb-16">
      <p className="text-xs mb-2 text-purple-950 dark:text-purple-200">{post.pubDate?.toDateString()}</p>
      <h2 className="text-2xl mb-2">
        <a href={`posts/${post.slug}`}>{post.title}</a>
      </h2>
      <p className="text-purple-950 dark:text-purple-200 mb-5">{post.description}</p>
      <a
        href={`posts/${post.slug}`}
        className="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>

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.

To handle the single post identified by a slug, use Next.js dynamic routes.

Create a new file, src/app/posts/[slug]/page.tsx, where the Next.js framework uses the directory [slug] to capture the name of the slug:

src/app/posts/[slug]/page.tsx
export default async function Post({ params }: { params: { slug: string } }) {
  return (
    <div className="w-full max-w-5xl mt-16">
      <p className="mb-2">
        <a href="/" className="text-purple-600">
          &larr; Back to blog
        </a>
      </p>

      <h1 className="text-3xl mb-2">{params.slug}</h1>
    </div>
  );
}

Above, the slug value is passed into the function via a params parameter and accessed using params.slug.

Next, let's update the page to bring in the Xata client and use the slug value to fetch the post from the database:

src/app/posts/[slug]/page.tsx
import { getXataClient } from '@/xata';

const xata = getXataClient();

export default async function Post({ params }: { params: { slug: string } }) {
  const post = await xata.db.Posts.filter({ slug: params.slug }).getFirst();

  return (
    <div className="w-full max-w-5xl mt-16">
      <p className="mb-2">
        <a href="/" className="text-purple-600">
          &larr; Back to blog
        </a>
      </p>

      <h1 className="text-3xl mb-2">{post?.title}</h1>
      <p className="text-sm mb-4 text-purple-950 dark:text-purple-200">{post?.pubDate?.toDateString()}</p>
      <p className="text-xl">{post?.description}</p>
    </div>
  );
}

The Xata client is imported and initialized in the same way as it is in the landing page by assigning the return value of getXataClient to a xata variable.

import { getXataClient } from '@/xata';

const xata = getXataClient();

Retrieve the single post from the database via the auto-generated Posts property. Use the filter function to query the table for a row where the slug column equals the value of params.slug. Finally, use the getFirst function to return the first (and only) Post result.

export default async function Post({ params }: { params: { slug: string } }) {
  const post = await xata.db.Posts.filter({ slug: params.slug }).getFirst();

  ...
}

Finally, the values, title, pubDate, and description, for the Post are added to the UI:

<h1 className="text-3xl mb-2">{post?.title}</h1>
<p className="text-sm mb-4 text-purple-950 dark:text-purple-200">
  {post?.pubDate?.toDateString()}
</p>
<p className="text-xl">{post?.description}</p>

The single blog post page will look as follows:

Single blog post
Single blog post

The last piece of functionality to be added 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. You don't need to change any configuration to enable search, you just need to use the TypeScript SDK search feature.

Let's add this functionality to the landing page:

src/app/page.tsx
import { getXataClient } from '@/xata';

const xata = getXataClient();

export default async function Home({ searchParams }: { searchParams: { q: string } }) {
  let posts = null;
  if (searchParams.q) {
    const { records } = await xata.db.Posts.search(searchParams.q, { fuzziness: 2 });
    posts = records;
  } else {
    posts = await xata.db.Posts.getAll();
  }

  return (
    <>
      <div className="w-full max-w-5xl mt-16">
        <form>
          <input
            name="q"
            defaultValue={searchParams.q}
            placeholder="Search..."
            className="w-full rounded-lg p-2 dark:text-purple-950"
          />
        </form>
      </div>
      ...
    </>
  );
}

Here's a breakdown of the changes introduced above.

First, update the main function to accept search parameters. You achieve this by having the function accept an object with a searchParams property. When a search has been performed, searchParams will optionally have a q property access via searchParams.q.

export default async function Home({ searchParams }: { searchParams: { q: string } }) {
  ...
}

Secondly, the landing page should list all blog posts if the searchParams.q property isn't present. However, if the property is present, a search is performed on the Posts table using the search function exposed on the auto-generated Posts property. Pass searchParams.q 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.

export default async function Home({ searchParams }: { searchParams: { q: string } }) {
  let posts = null;
  if (searchParams.q) {
    const { records } = await xata.db.Posts.search(searchParams.q, { fuzziness: 2 });
    posts = records;
  } else {
    posts = await xata.db.Posts.getAll();
  }

  ...
}

The third and last change is to add a <form> to the page to allow a search value to be entered and submitted. 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}. Since this is precisely the behavior you need, and you've already updated the page function to accept the object with a searchParams property, everything is in place.

<div className="w-full max-w-5xl mt-16">
  <form>
    <input
      name="q"
      defaultValue={searchParams.q}
      placeholder="Search..."
      className="w-full rounded-lg p-2 dark:text-purple-950"
    />
  </form>
</div>
Full-text fuzzy search
Full-text fuzzy search

The application now supports listing posts, viewing single posts via a dynamic route, and full-text fuzzy search of posts.

In this guide, you've learned that Next.js App Router 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 Next.js dynamic routes where a slug was passed and used with xata.db.Posts.filter({ slug: params.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 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 Next.js 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