Building data-rich apps with Next.js and Tigris

Ovais Tariq - Oct 27 '22 - - Dev Community

Next.js gives you the best developer experience with all the features you need to build modern, fast production-ready applications. Tigris is the perfect companion for Next.js as it is similarly built with developer experience in mind and is truly serverless: build data-rich features, seamlessly implement search, and easily use it with serverless functions, all without needing to do Ops.

Next.js and Tigris

Now with the introduction out of the way, it is time to demonstrate how.

This is the first of a series of blog posts where we will demonstrate how easy it is to build Next.js apps with Tigris. We will build a to-do list app and deploy it to Vercel. The to-do list app will have the following features:

  • add to-do items to the list
  • update to-do items as completed
  • delete to-do items
  • search for to-do items in the list

To follow along with this tutorial you can get the code from the GitHub repo. This is how the to-do app will look once its deployed: https://tigris-nextjs-starter-kit.vercel.app/

Prerequisites

For this tutorial you'll need:

  1. GitHub account (sign up for free)
  2. Tigris Cloud account (sign up for free)
  3. Vercel account (sign up for free) to deploy app
  4. Node.js 16+
  5. npm and npx

Setting up Tigris

The CLI is the easiest way to get started with Tigris. Let's install the CLI first:

  • macOS
  • linux
brew install tigrisdata/tigris/tigris-cli
Enter fullscreen mode Exit fullscreen mode

The next step is to generate credentials: clientIdclientSecret. These credentials will be used by the Next.js to-do list app to connect and authenticate with Tigris. Let's login to your Tigris Cloud account and create the application credentials.

tigris login
Enter fullscreen mode Exit fullscreen mode
tigris create application todo_list "to-do list application"
Enter fullscreen mode Exit fullscreen mode

Once the command is executed, it would create the application credentials (clientID and clientSecret) and print it on the screen. Make a note of id and secret from the output.

Alternatively, you can generate the application credentials through the Tigris Cloud web console.

Deploying the to-do list app to Vercel

We will start off first by deploying the pre-prepared to-do list app to Vercel from the GitHub repo. Then once it is deployed and running, we will explore the code in detail.

Create a project on Vercel

Vercel makes it easier to deploy Git projects with a few clicks.

Hit the following Deploy button to get started with the Vercel workflow to clone the repo to your account

Deploy with Vercel

This should take you to Vercel to the "Create Git Repository" step

Vercel create repo

Pick a name for your new Git repo and then you'll configure the environment variables needed to connect the to-do list app to Tigris. This is where we'd need the clientID and clientSecret generated in the setting up Tigris section above.

TIGRIS_URI=api.preview.tigrisdata.cloud
TIGRIS_CLIENT_ID=<your_client_id_here>
TIGRIS_CLIENT_SECRET=<your_client_secret_here>
Enter fullscreen mode Exit fullscreen mode

Vercel environment

Once you have entered the environment variables hit the Deploy button. That's it!

Once the deployment completes, continue to your project dashboard on Vercel where you'll find URL for your to-do list app

Vercel project dashboard

🎉 All done. Visit the URL in browser to access your to-do list app and play around. 🎉

Now let's continue to explore the code for the to-do list app to see how easily Tigris can be integrated with Next.js.

Code walk-through

This section will elaborate on important aspects of the to-do list app you just deployed. Let's glance over the important components of the project.

File structure

|-- package.json
|-- lib
|   |-- schema.ts
|   |-- tigris.ts
|-- pages
    |-- index.tsx
    |-- api
        |-- item
        |   |-- [id].ts
        |-- items
            |-- index.ts
            |-- search.ts
Enter fullscreen mode Exit fullscreen mode

Tigris schema definition - lib/schema.ts

With Tigris it all starts with the data model!

Tigris stores data records as documents. Documents are analogous to JSON objects but Tigris stores them in an optimized binary format. Documents are grouped together in collections. The to-do list app has a single collection todoItems that stores the to-do items. The first thing you would do is define the schema that enables you to structure the data. The schema can be easily evolved in a lightweight way without costly rebuild operations.

The schema for the collection is defined in lib/schema.ts as follows:

lib/schema.ts

import {
  TigrisCollectionType,
  TigrisDataTypes,
  TigrisSchema,
} from "@tigrisdata/core/dist/types";

export const DB_NAME = "tigrisTodoApp";
export const COLLECTION_NAME = "todoItems";

export interface TodoItem extends TigrisCollectionType {
  id?: number;
  text: string;
  completed: boolean;
}

export const TodoItemSchema: TigrisSchema<TodoItem> = {
  id: {
    type: TigrisDataTypes.INT32,
    primary_key: { order: 1, autoGenerate: true },
  },
  text: { type: TigrisDataTypes.STRING },
  completed: { type: TigrisDataTypes.BOOLEAN },
};
Enter fullscreen mode Exit fullscreen mode

Connecting to Tigris - lib/tigris.ts

This file loads the environment variables you specified previously in creating a Vercel project section and uses them to configure the Tigris client. This client is used to manage all the Tigris operations from here on. Also, note how we are caching the client instance so that it can be used for subsequent requests.

lib/tigris.ts

import { DB, Tigris, TigrisClientConfig } from "@tigrisdata/core";
import { DB_NAME } from "./schema";

if (!process.env.TIGRIS_URI) {
  throw new Error("Cannot find TIGRIS_URI environment variable ");
}

const tigrisUri = process.env.TIGRIS_URI;
const clientConfig: TigrisClientConfig = { serverUrl: tigrisUri };

if (process.env.TIGRIS_CLIENT_ID) {
  clientConfig.clientId = process.env.TIGRIS_CLIENT_ID;
}
if (process.env.TIGRIS_CLIENT_SECRET) {
  clientConfig.clientSecret = process.env.TIGRIS_CLIENT_SECRET;
}

declare global {
  var tigrisDb: DB;
}

let tigrisDb: DB;

if (process.env.NODE_ENV === "development") {
  if (!global.tigrisDb) {
    // re-use the same connection in dev
    const tigrisClient = new Tigris(clientConfig);
    global.tigrisDb = tigrisClient.getDatabase(DB_NAME);
  }
  tigrisDb = global.tigrisDb;
} else {
  const tigrisClient = new Tigris(clientConfig);
  tigrisDb = tigrisClient.getDatabase(DB_NAME);
}

// export to share DB across modules
export default tigrisDb;
Enter fullscreen mode Exit fullscreen mode

Creating the database and collection - scripts/setup.ts

This app ensures that the database and collection are automatically created at build time. There is no need to manually create them. Databases and collections can be created in an idempotent way instantaneously, i.e. it is safe to run the creation operation during build time. More details on this automatic setup can be found in scripts/setup.ts.

Pages

Let's take a look at fetchListItems() in this React component that loads and renders the to-do list items.

// Fetch Todo List
const fetchListItems = () => {
  setIsLoading(true);
  setIsError(false);

  fetch("/api/items")
    .then((response) => response.json())
    .then((data) => {
      setIsLoading(false);
      if (data.result) {
        setViewMode("list");
        setTodoList(data.result);
      } else {
        setIsError(true);
      }
    })
    .catch(() => {
      setIsLoading(false);
      setIsError(true);
    });
};
Enter fullscreen mode Exit fullscreen mode

Evidently this React component is only rendering the items returned by /api/items.

Similarly, the addTodoItem(), to add a to-do list item, simply makes a POST request to /api/items.

// Add a new to-do item
const addToDoItem = () => {
  if (queryCheckWiggle()) {
    return;
  }
  setIsLoading(true);

  fetch("/api/items", {
    method: "POST",
    body: JSON.stringify({ text: textInput, completed: false }),
  }).then(() => {
    setIsLoading(false);
    setTextInput("");
    fetchListItems();
  });
};
Enter fullscreen mode Exit fullscreen mode

We will now dive into the API routes to see how these are integrated with Tigris that is powering our application.

TIGRIS AND SERVERLESS FUNCTIONS
All the API routes are deployed as Serverless Functions. Tigris is serverless itself and natively supports HTTP. This makes it a perfect fit for Serverless Functions.

API routes to find and add items

All the Next.js API routes are defined under /pages/api/. We have three files: /pages/api/items/index.ts/pages/api/items/search.ts and /pages/api/item/[id].ts that expose following endpoints:

  • GET /api/items to get an array of to-do items as Array<TodoItem>
  • POST /api/items to add an item to the list
  • GET /api/items/search?q=query to find and return items matching the given query
  • GET /api/item/{id} to fetch an item
  • PUT /api/item/{id} to update the given item
  • DELETE /api/item/[id] to delete an item

Let's look at the /api/items api that supports both GET and POST handlers.

pages/api/items/index.ts

import type { NextApiRequest, NextApiResponse } from 'next'
import { COLLECTION_NAME, TodoItem } from '../../../lib/schema'
import tigrisDb from '../../../lib/tigris'

type Response = {
  result?: Array<TodoItem>,
  error?: string
}

// GET /api/items -- gets items from collection
// POST /api/items {ToDoItem} -- inserts a new item to collection
export default async function handler (
  req: NextApiRequest,
  res: NextApiResponse<Response>
) {
  switch (req.method) {
    case 'GET':
      await handleGet(req, res)
      break
    case 'POST':
      await handlePost(req, res)
      break
    default:
      res.setHeader('Allow', ['GET', 'POST'])
      res.status(405).end(`Method ${req.method} Not Allowed`)
  }
}

...
Enter fullscreen mode Exit fullscreen mode

handleGet() method is fetching and returning items from Tigris collection, let's take a look at its implementation. You will see how easy it is to fetch data from Tigris.

pages/api/items/index.ts

async function handleGet(req: NextApiRequest, res: NextApiResponse<Response>) {
  try {
    const itemsCollection = tigrisDb.getCollection<TodoItem>(COLLECTION_NAME);
    const cursor = itemsCollection.findMany();
    const items = await cursor.toArray();
    res.status(200).json({ result: items });
  } catch (err) {
    const error = err as Error;
    res.status(500).json({ error: error.message });
  }
}
Enter fullscreen mode Exit fullscreen mode

The itemsCollection.findMany() function sends a query to Tigris and returns a cursor to fetch results from collection.

Let's look at handlePost() implementation that inserts a TodoItem in collection by using the insertOne() function.

pages/api/items/index.ts

async function handlePost(req: NextApiRequest, res: NextApiResponse<Response>) {
  try {
    const item = JSON.parse(req.body) as TodoItem;
    const itemsCollection = tigrisDb.getCollection<TodoItem>(COLLECTION_NAME);
    const inserted = await itemsCollection.insertOne(item);
    res.status(200).json({ result: [inserted] });
  } catch (err) {
    const error = err as Error;
    res.status(500).json({ error: error.message });
  }
}
Enter fullscreen mode Exit fullscreen mode

API route to search items

Tigris makes it really easy to implement search within your applications by providing an embedded search engine that makes all your data instantly searchable.

Let's take a look at the search handler to see how easy it is to add powerful real-time search functionality. The itemsCollection.search functions sends a search query to Tigris and fetches the documents that match the query.

TIGRIS REAL-TIME SEARCH
Note how you did not have to setup Elasticsearch, or configure search indexes. It was all taken care for you automatically.

pages/api/items/search.ts

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  const query = req.query["q"];
  if (query === undefined) {
    res.status(400).json({ error: "No search query found in request" });
    return;
  }
  try {
    const itemsCollection = tigrisDb.getCollection<TodoItem>(COLLECTION_NAME);
    const searchRequest: SearchRequest<TodoItem> = { q: query as string };
    const searchResult = await itemsCollection.search(searchRequest);
    const items = new Array<TodoItem>();
    for (const hit of searchResult.hits) {
      items.push(hit.document);
    }
    res.status(200).json({ result: items });
  } catch (err) {
    const error = err as Error;
    res.status(500).json({ error: error.message });
  }
}
Enter fullscreen mode Exit fullscreen mode

Summary

In this tutorial, you deployed a to-do list Next.js app that uses Tigris as the backend. You saw all the powerful functionality that Tigris provides, and how easy it is to use it within Serverless Functions.

Tigris is the easiest way to work with data in your Next.js applications. Tigris and Next.js provide developers with the fastest way to build fast, data-rich and highly-responsive applications.

You can find the complete source code for this tutorial GitHub repo, feel free to raise issues or contribute to the project.

Happy learning!


Tigris is the data platform built for Next.js applications! Use it as a scalable, ACID transactional, real-time backend for your serverless applications. Build rich features with dynamic data without worrying about slow database queries or missing indexes. Seamlessly implement search within your applications with its embedded search engine. Connect serverless functions with its event streams to build highly responsive applications that scale automatically.

Sign up for the beta

Get early access and try out Tigris for your next Next.js application. Join our Slack or Discord community to ask any questions you might have.

. . . . . . . . . . . . . . . . . . . . . .