Get started with SolidStart and Xata
In this guide, you'll learn how to add Xata database and search functionality to a SolidStart application. You'll build the following basic blog application features:
- List all blog posts
- Retrieve and view a single blog post
- Full-text fuzzy search of blog posts
Although this application is a simple blog, you can apply these basics to other types of SolidStart applications.
The completed SolidStart and Xata code for this
guide is available via the Xata examples
repo on GitHub.
Install the Xata CLI:
npm install -g @xata.io/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 SolidStart application, accepting the default prompt options:
npm create solid@latest xata-solidstart
Once the command has completed, go to the xata-solidstart
directory, install the dependencies, and run the application:
cd xata-solidstart
npm install
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 SolidStart 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:
XATA_API_KEY=YOUR_API_KEY_HERE
XATA_BRANCH=main
To inform the TypeScript typechecking about these new environment variables, update src/env.d.ts
as follows:
interface ImportMetaEnv {
readonly XATA_API_KEY: string;
readonly XATA_BRANCH?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Since you selected TypeScript support, 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:
- Auto-generate a schema based on column headings for names and data types inferred from the column values
- 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
curl.exe --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:
Click Schema to see the schema definition with the inferred data types:
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 your SolidStart 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.
The first step to add some styling to the application is to add Tailwind CSS.
Install and initialize Tailwind CSS:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Update the content
property in tailwind.config.js
:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {}
},
plugins: []
};
Next, replace the contents of src/root.css
with the following:
@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 src/root.tsx
to add some shared structure across application pages. The code will end up as follows:
// @refresh reload
import { Suspense } from 'solid-js';
import { Body, ErrorBoundary, FileRoutes, Head, Html, Meta, Routes, Scripts, Title } from 'solid-start';
import './root.css';
export default function Root() {
return (
<Html lang="en">
<Head>
<Title>Get started with Xata and SolidStart</Title>
<Meta charset="utf-8" />
<Meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<Body>
<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 SolidStart</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>
<Suspense>
<ErrorBoundary>
<Routes>
<FileRoutes />
</Routes>
</ErrorBoundary>
</Suspense>
<Scripts />
</main>
</Body>
</Html>
);
}
You may need to start and stop your development server for the Tailwind CSS changes to be picked up.
Now, you are ready to integrate Xata into the SolidStart codebase. Start by stripping back the landing page,
src/routes/index.tsx
, to a bare template:
export default function Home() {
return (
<>
<div class="w-full max-w-5xl mt-16">List of blog posts</div>
</>
);
}
Next, update the page to get all the posts using Xata, and list them within the page:
import { Match, Switch, For } from 'solid-js';
import { useRouteData } from 'solid-start';
import { createServerData$ } from 'solid-start/server';
import { XataClient } from '~/xata';
export function routeData() {
return createServerData$(async () => {
const xata = new XataClient({
apiKey: import.meta.env.XATA_API_KEY,
branch: import.meta.env.XATA_BRANCH
});
let posts = await xata.db.Posts.getAll();
return { posts };
});
}
export default function Home() {
const resource = useRouteData<typeof routeData>();
const data = resource();
const posts = data?.posts;
return (
<>
<div class="w-full max-w-5xl mt-16">
<Switch>
<Match when={!posts}>
<p>No blog posts found</p>
</Match>
<Match when={posts && posts.length > 0}>
<For each={posts}>
{(post) => (
<div class="mb-16">
<p class="text-xs mb-2 text-purple-950 dark:text-purple-200">{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 →
</a>
</div>
)}
</For>
</Match>
</Switch>
</div>
</>
);
}
The breakdown of what's happening in the code above is as follows.
Import Match
and Switch
, and
For
from Solid to be used when rendering the UI. Then import
useRouteData
and the server-only
createServerData$
function to be used when retrieving data from Xata.
import { Match, Switch, For } from 'solid-js';
import { useRouteData } from 'solid-start';
import { createServerData$ } from 'solid-start/server';
Next, define and export a routeData
function. This returns a call to createServerData$
, a SolidStart function
specifically for accessing resources only available on the server, like databases. This function provides data to the route when rendering.
Within that function, create an instance of the XataClient
, passing in the values stored in environment variables for apiKey
and branch
. Then,
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. Finally, return the posts
as the function return value.
import { XataClient } from '~/xata';
export function routeData() {
return createServerData$(async () => {
const xata = new XataClient({
apiKey: import.meta.env.XATA_API_KEY,
branch: import.meta.env.XATA_BRANCH
});
let posts = await xata.db.Posts.getAll();
return { posts };
});
}
getAll()
returns all the records in the query results. This is dangerous on large tables (more than 10,000 records),
as it will potentially load a lot of data into memory and create a lot of requests to the server. In most situations,
you should use getMany()
or getPaginated()
. See the querying
documentation for more information.
Finally, use the useRouteData
hook within the Home
function to retrieve the posts
data.
Then, update the UI to display the results. Make use of Solid's Switch
control flow component in combination with Match
;
If no records are present, show a message saying, "No blog posts found". Otherwise, loop through the posts
using <For>
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:
export default function Home() {
const resource = useRouteData<typeof routeData>();
const data = resource();
const posts = data?.posts;
return (
<>
<div class="w-full max-w-5xl mt-16">
<Switch>
<Match when={!posts}>
<p>No blog posts found</p>
</Match>
<Match when={posts && posts.length > 0}>
<For each={posts}>
{(post) => (
<div class="mb-16">
<p class="text-xs mb-2 text-purple-950 dark:text-purple-200">{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 →
</a>
</div>
)}
</For>
</Match>
</Switch>
</div>
</>
);
}
This results in the page looking like the following:
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 posts identified by a slug
, make use of SolidStart
dynamic routing.
Create a new file, src/routes/posts/[slug].tsx
, where the SolidStart framework uses the filename segment [slug]
to capture
the name of the slug:
import { Title } from 'solid-start';
import { useParams, useRouteData } from 'solid-start';
import { createServerData$ } from 'solid-start/server';
import { XataClient } from '~/xata';
export function routeData() {
return createServerData$(async () => {
const xata = new XataClient({
apiKey: import.meta.env.XATA_API_KEY,
branch: import.meta.env.XATA_BRANCH
});
const params = useParams();
return await xata.db.Posts.filter({ slug: params.slug }).getFirst();
});
}
export default function Post() {
const data = useRouteData<typeof routeData>();
const post = data();
return (
<>
<Title>{post?.title}</Title>
<div class="w-full max-w-5xl mt-16">
<p class="mb-2">
<a href="/" class="text-purple-600">
← 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">{post?.pubDate?.toDateString()}</p>
<p class="text-xl">{post?.description}</p>
</div>
</>
);
}
Here's a walkthrough of what this does.
Import useParams
to enable access to the value of the slug
. Import useRouteData
and createServerData$
to enable
data to be loaded.
import { Title } from 'solid-start';
import { useParams, useRouteData } from 'solid-start';
import { createServerData$ } from 'solid-start/server';
The Title
component is imported from solid-start
to allow the page title to be set. The routeData
and
createServerData$
functions do the same job as the landing page.
Instantiate a new XataClient
instance, get the page parameters using the useParams
hook, and then use the
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 params.slug
. Use the
getFirst
function to access
the first (and only) Post result and return the post as the createServerData$
return value.
import { XataClient } from '~/xata';
export function routeData() {
return createServerData$(async () => {
const xata = new XataClient({
apiKey: import.meta.env.XATA_API_KEY,
branch: import.meta.env.XATA_BRANCH
});
const params = useParams();
return await xata.db.Posts.filter({ slug: params.slug }).getFirst();
});
}
Finally, retrieve the post
data using the useLoaderData
hook. Then, use the post
data to set
the page title via <Title />
and update the rest of the page UI to contain all the information for the single Post:
export default function Post() {
const data = useRouteData<typeof routeData>();
const post = data();
return (
<>
<Title>{post?.title}</Title>
<div class="w-full max-w-5xl mt-16">
<p class="mb-2">
<a href="/" class="text-purple-600">
← 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">{post?.pubDate?.toDateString()}</p>
<p class="text-xl">{post?.description}</p>
</div>
</>
);
}
You may need to restart your development server to see the changes.
The single blog post page will look as follows:
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, you just need to use the TypeScript SDK search feature.
Add this functionality to the landing page:
import { useLocation } from '@solidjs/router';
import { Match, Switch, For } from 'solid-js';
import { useRouteData } from 'solid-start';
import { createServerData$ } from 'solid-start/server';
import { XataClient } from '~/xata';
export function routeData() {
return createServerData$(async () => {
const xata = new XataClient({
apiKey: import.meta.env.XATA_API_KEY,
branch: import.meta.env.XATA_BRANCH
});
const location = useLocation();
const search = location.query.q;
let posts = null;
if (search) {
posts = await xata.db.Posts.search(search, { fuzziness: 2 });
} else {
posts = await xata.db.Posts.getAll();
}
return { posts, search };
});
}
export default function Home() {
const resource = useRouteData<typeof routeData>();
const data = resource();
const posts = data?.posts;
const search = data?.search || '';
return (
<>
<div class="w-full max-w-5xl mt-16">
<form action="/">
<input
name="q"
value={search}
placeholder="Search..."
class="w-full rounded-lg border-2 p-2 dark:text-purple-950"
/>
</form>
</div>
...
</>
);
}
Here are all the changes made in the above code.
First, import useLocation
from @solidjs/router
and use it within createServerData$
to retrieve the
value of a q
querystring parameter, assigned 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.
Update the return value to include the search
value in addition to the posts
.
import { useLocation } from '@solidjs/router';
import { Match, Switch, For } from 'solid-js';
import { useRouteData } from 'solid-start';
import { createServerData$ } from 'solid-start/server';
import { XataClient } from '~/xata';
export function routeData() {
return createServerData$(async () => {
const xata = new XataClient({
apiKey: import.meta.env.XATA_API_KEY,
branch: import.meta.env.XATA_BRANCH
});
const location = useLocation();
const search = location.query.q;
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, search };
});
}
The last change enables the user to input and submit a search.
Begin by retrieving the search
value from the resource
returned via the useRouteData
hook.
Next, add a <form>
to the page to allow a search value to be entered and submitted. Set the value
of the <input name="q" />
to be the value of search
so if there is an active search, the user has some UI feedback.
The default submit behavior of a form is to perform a GET
request to the current URL with any form inputs added to the
querystring in the format {url}/?{input-name}={input-value}
. For our search form, the result of a submission is
a GET
request in the format ?q={q-value}
. Since this is precisely the behavior required, and the check for the q
querystring search value has been implemented already, everything is in place.
export default function Home() {
const resource = useRouteData<typeof routeData>();
const data = resource();
const posts = data?.posts;
const search = data?.search || '';
return (
<>
<div class="w-full max-w-5xl mt-16">
<form action="/">
<input
name="q"
value={search}
placeholder="Search..."
class="w-full rounded-lg border-2 p-2 dark:text-purple-950"
/>
</form>
</div>
...
</>
);
}
The result of the changes means your landing page UI looks like this:
The application now supports listing posts, viewing single posts via a dynamic segment, and full-text fuzzy search of posts.
In this guide, you've learned that SolidStart 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
) usingxata 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 SolidStart 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: