Add Tigris to an existing Next.js Application
In this guide we will take an existing Next.js blog-starter application. This application is static in nature. We will make it dynamic by adding comments feature that will use Tigris for data storage and retrieval.
Prerequisites
System requirements
- Node.js 18.13.0 or newer
- MacOS, Windows, and Linux are supported
Sign up
To get started, go to the Tigris console and sign up.
Create Next.js app using blog-starter template
Then, create a new Next.js application using the blog-starter template
npx create-next-app@latest --example blog-starter
$ npx create-next-app@latest --example blog-starter
Need to install the following packages:
create-next-app@13.0.7
Ok to proceed? (y) y
✔ What is your project named? … my-nextjs-blog
Creating a new Next.js app in <Current directory>/my-nextjs-blog.
Downloading files for example blog-starter. This might take a moment.
Installing packages. This might take a couple of minutes.
added 180 packages, and audited 181 packages in 13s
82 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Initialized a git repository.
Success! Created my-nextjs-blog at <Current directory>/my-nextjs-blog
Inside that directory, you can run several commands:
npm run dev
Starts the development server.
npm run build
Builds the app for production.
npm start
Runs the built app in production mode.
We suggest that you begin by typing:
cd my-nextjs-blog
npm run dev
This is a simple command to quickly start building a new Next.js application
based off a template. It may ask you a project name to download the project
resources. We choose my-nextjs-blog
as our project name.
Change to my-nextjs-blog
directory and install the Tigris SDK:
cd my-nextjs-blog
npm install @tigrisdata/core@latest
Create Tigris Project
Now let's create a Tigris project for our blogging application. Click on the
Create a new project
button from the
Use MyNextjsBlog
as the project name.
Tigris will generate the credentials for your application, and you will be shown a command to generate application code with Tigris pre-configured.
As we are not creating a new application, rather adding Tigris to an existing application, we don't need to run this command.
Let's copy the project name
, uri
, clientId
and clientSecret
from the
command shown and close the modal.
Configure environment variables
Now that we have created the project, lets set up the environment variables which will allow the blog application to connect with Tigris.
We will use the project name, URI, clientId and clientSecret from the previous step
TIGRIS_URI=tigris_uri
TIGRIS_PROJECT=MyNextjsBlog
TIGRIS_CLIENT_ID=ftSUj9B5czFW79s9M6YUkxKE3H4WeRyY
TIGRIS_CLIENT_SECRET=DOxxx
TIGRIS_DB_BRANCH=main
Configure tsconfig.json
Let's ensure that the required settings are enabled in tsconfig.json
.
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "es5",
"module": "esnext",
"jsx": "preserve",
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"noEmit": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true
},
"ts-node": {
"compilerOptions": {
"module": "commonjs"
}
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}
Create a Tigris Collection
Every Tigris project comes with a pre-configured database and stores data records as documents. Documents are analogous to JSON objects grouped in a Collection.
Let's create a data model to define the collection. We will be storing the data
models in the directory db/models
.
import {
Field,
PrimaryKey,
TigrisCollection,
TigrisDataTypes,
} from "@tigrisdata/core";
@TigrisCollection("comments")
export class Comments {
@PrimaryKey(TigrisDataTypes.INT32, { order: 1, autoGenerate: true })
id!: number;
@Field()
name: string;
@Field()
content: string;
@Field(TigrisDataTypes.DATE_TIME)
date: string;
}
Let's see what is going on here. You've imported the Tigris @TigrisCollection
decorator, which accepts a collection name as an argument to create a
collection. The TigrisDataTypes
type gives you access to the Tigris data
types. @Field
decorator defines the collection fields and the @PrimaryKey
decorator define the primary of the collection.
Create a setup script
Now we will create a setup script which will take care of creating the collection for the data model defined above.
import { loadEnvConfig } from "@next/env";
import { Tigris } from "@tigrisdata/core";
import { Comments } from "./db/models/comments";
async function main() {
// load environment variables
loadEnvConfig(process.cwd());
// create collections
const tigrisClient = new Tigris();
await tigrisClient.registerSchemas([Comments]);
}
main()
.then(async () => {
console.log("Setup complete ...");
process.exit(0);
})
.catch(async (e) => {
console.error(e);
process.exit(1);
});
Here we have used the registerSchemas
method, which takes in an array of all
the model classes defined in your db/model
directory. In our case we have a
single model class Comments
. When this script is executed Tigris will create a
collection named comments
corresponding to the model class Comments
.
Next, let's add setup.ts
as one of the scripts in package.json
.
"scripts": {
"predev": "npm run setup",
"dev": "next",
"build": "next build",
"postbuild": "npm run setup",
"setup": "npx ts-node setup.ts",
"start": "next start",
"typecheck": "tsc"
}
Now, whenever you run your application your collections will always be in sync with the data models defined in the application.
Instantiate the Tigris client
Next, initialize the Tigris client and export it to share it across all the modules.
import { DB, Tigris } from "@tigrisdata/core";
const tigrisClient = new Tigris();
const tigrisDB: DB = tigrisClient.getDatabase();
// export to share DB across modules
export default tigrisDB;
Create CRUD Operations
You can now create and execute CRUD operations against the comments
collection. These endpoints will be used by the client component of your
application to perform CRUD operations in Tigris. Create a
api/comments/index.ts
file in the pages
directory to define your API
endpoints.
⌲ GET /api/comments
to fetch all blogs.
import type { NextApiRequest, NextApiResponse } from "next";
import { Comments } from "../../../db/models/comments";
import tigrisDB from "../../../lib/tigris";
type Response = {
result?: Array<Comments>;
error?: string;
};
async function handleGet(req: NextApiRequest, res: NextApiResponse<Response>) {
try {
const commentsCollection = tigrisDB.getCollection<Comments>(Comments);
const cursor = commentsCollection.findMany();
const comments = await cursor.toArray();
res.status(200).json({ result: comments });
} catch (err) {
res.status(500).json({ error: err.message });
}
}
Here we used a simple findMany()
function to read documents from a Tigris
collection and returned them as json.
⌲ POST /api/comments
to add a new blog data.
Next, create a POST
handler in the same file pages/api/comments/index.ts
where we added the GET
handler above. This will store a new comment in the
Comments
collection.
//...
async function handlePost(req: NextApiRequest, res: NextApiResponse<Response>) {
try {
const comment = JSON.parse(req.body) as Comments;
const commentsCollection = tigrisDB.getCollection<Comments>(Comments);
const inserted = await commentsCollection.insertOne(comment);
res.status(200).json({ result: [inserted] });
} catch (err) {
res.status(500).json({ error: err.message });
}
}
//...
Finally, add the handler function to define the routes for the API endpoints:
//...
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`);
}
}
//...
⌲ GET /api/comments/search
to find a comment matching the given query
The search endpoint will allow users to find comments in the collection. Let's
add this endpoint that accepts a query parameter and responds with matching
results. Eg. GET /api/comments/search?q="query"
Create a search.ts
file in the pages/api/comments
directory and add the
code:
import { NextApiRequest, NextApiResponse } from "next";
import { Comments } from "../../../db/models/comments";
import { SearchRequest } from "@tigrisdata/core/dist/search/types";
import tigrisDb from "../../../lib/tigris";
type Data = {
result?: Array<Comments>;
error?: string;
};
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 commentCollection = tigrisDb.getCollection<Comments>(Comments);
const searchRequest: SearchRequest<Comments> = { q: query as string };
const searchResult = await commentCollection.search(searchRequest);
const comment = new Array<Comments>();
for (const hit of searchResult.hits) {
comment.push(hit.document);
}
res.status(200).json({ result: comment });
} catch (err) {
res.status(500).json({ error: err.message });
}
}
Built-in search Tigris makes it really easy to implement search within your applications by providing a serverless full-text search engine that makes all your data instantly searchable.
Use in the components
Now you can request the API to fetch and manipulate the data from your Tigris
database in clients components. First, create comment.tsx
and
each-comments.tsx
files in the components
directory to create the components
for the comments.
Inside the components/each-comments.tsx
file, add the following code:
import { Comments } from "../db/models/comments";
type Props = {
comment: Comments;
};
const EachComments = ({ comment }: Props) => {
return (
<div className="relative grid grid-cols-1 gap-4 p-4 mb-8 border rounded-lg bg-white shadow-lg">
<div className="relative flex gap-4">
<img
src="https://icons.iconarchive.com/icons/diversity-avatars/avatars/256/charlie-chaplin-icon.png"
className="relative rounded-lg -top-8 -mb-4 bg-white border h-20 w-20"
alt=""
loading="lazy"
/>
<div className="flex flex-col w-full">
<div className="flex flex-row justify-between">
<p className="relative text-xl whitespace-nowrap truncate overflow-hidden">
{comment.content}
</p>
<a className="text-gray-500 text-xl" href="#">
<i className="fa-solid fa-trash"></i>
</a>
</div>
<p className="text-gray-400 text-sm">{comment.date}</p>
</div>
</div>
<p className="-mt-4 text-gray-500">{comment.name}</p>
</div>
);
};
export default EachComments;
Next, let's define the React component that is rendered on the browser using the
API endpoints we created in the previous section. Add the code below to the
components/comment.tsx
file:
import { useEffect, useState } from "react";
import { Comments } from "../db/models/comments";
import EachComments from "./each-comments";
const Comment = () => {
const [comments, setComments] = useState<Comments[]>([]);
const [isError, setIsError] = useState(false);
const [nameInput, setNameInput] = useState("");
const [commentInput, setCommentInput] = useState("");
const [searchInput, setSearchInput] = useState("");
// fetch list of comments from `/api/comments` endpoint which in turn
// reads from the comments collection
const fetchComments = () => {
fetch("/api/comments")
.then((response) => response.json())
.then((data) => {
if (data.result) {
setComments(data.result);
} else {
setIsError(true);
}
})
.catch(() => {
setIsError(true);
});
};
// create a new comment by sending a POST request to `/api/comments` endpoint
const addComment = () => {
fetch("/api/comments", {
method: "POST",
body: JSON.stringify({
name: nameInput,
content: commentInput,
date: new Date().toJSON(),
}),
}).then(() => {
setCommentInput("");
setNameInput("");
fetchComments();
});
};
// query the collection for a comments by sending a GET request to
// the `/api/comments/search` endpoint
const searchQuery = () => {
fetch(`/api/comments/search?q=${encodeURI(searchInput)}`, {
method: "GET",
})
.then((response) => response.json())
.then((data) => {
if (data.result) {
setComments(data.result);
}
});
};
useEffect(() => {
fetchComments();
}, []);
// render the comments and add the functions defined above to their
// respective event handlers
return (
<div className="max-w-2xl mx-auto">
<input
className="border border-gray-300 text-gray-900 dark:text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 mr-2 mb-6"
placeholder="Search..."
onChange={(e) => setSearchInput(e.target.value)}
onKeyUp={searchQuery}
/>
{comments.length ? (
comments.map((comment) => (
<EachComments comment={comment} key={comment.id} />
))
) : (
<p className="relative grid grid-cols-1 gap-4 p-4 mb-8 border rounded-lg bg-white shadow-lg">
No comments...
</p>
)}
<h3>Leave a comment</h3>
<div className="mb-6 mt-6">
<input
className="border border-gray-300 text-gray-900 dark:text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Name:"
onChange={(e) => setNameInput(e.target.value)}
value={nameInput}
/>
</div>
<div className="mb-6 mt-6">
<textarea
className="border border-gray-300 text-gray-900 dark:text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Say something..."
onChange={(e) => setCommentInput(e.target.value)}
value={commentInput}
/>
</div>
<button
type="submit"
className="bg-gray-600 hover:bg-gray-800 focus:ring-4 focus:ring-blue-300 font-medium text-white rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center mb-3"
onClick={addComment}
>
Post comment
</button>
{isError && (
<p className="text-red-600">Something went wrong: {isError}</p>
)}
</div>
);
};
export default Comment;
Lastly, render the <Comment/>
component inside the <Container>
component in
the pages/posts/[slug].tsx
file:
//...
import Comment from '../../components/comment'
//...
export default function Post({ ... }: ... {
return (
<Layout preview={preview}>
<Container>
//...
<Comment/>
</Container>
</Layout>
)
}
//...
Run the application
Our blog application is now ready with the shiny new comments functionality. Let's run the application.
npm run dev
$ npm run dev
> predev
> npm run setup
> setup
> npx ts-node setup.ts
info - Using reflection to infer type of Comments#name
info - Using reflection to infer type of Comments#content
Loaded env from <Current Directory>/projects/my-nextjs-blog/.env.local
info - Using Tigris at: api.preview.tigrisdata.cloud:443
event - Creating collection: 'comments' in project: 'MyNextjsBlog'
Setup complete ...
> dev
> next
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
info - Loaded env from <Current Directory>/projects/my-nextjs-blog/.env.local
event - compiled client and server successfully in 302 ms (165 modules)
wait - compiling / (client and server)...
event - compiled client and server successfully in 1341 ms (581 modules)
This should startup the application and make it available on http://localhost:3000
Head over to one of the blog posts and you will see the comments form.
Now, if you go back to your Tigris Cloud console, you will see the collection for the project and as you will add the comments you will see the data show up.
In this guide we have seen how to use Tigris to make an existing Next.js application dynamic.
Next steps
Below are some of the other guides that you can check out: