Next.js + Appwrite BaaS
Integrating a Next.js WebApp with Appwrite's Backend-as-a-Service SDK.
In this article, we will explore the process of integrating a Next.js project with Appwrite as a BaaS provider. Appwrite simplifies the backend development process by providing a range of ready-to-use features, including authentication, database management, file storage, and Appwrite's real-time socket-like connectivity, which have all been implemented in this Next.js project: https://next-gen-patients.vercel.app and this complementary project https://next-gen-doctors.vercel.app.
Name of Project: NextGenPatients
Team Members: Chuku Success(Chuku-Success) and Mainasara Olulanke (Mainasara Olulanke)
Description of Project: NextGenPatients is an open-source project aimed at making medical advice and consultation easily accessible. Features of the app are;
Appointment Booking: Schedule online medical appointments with ease, eliminating the need for unnecessary physical visits.
Virtual Consultations: Connect with doctors through chat, to seek medical advice.
Health Information DB: Store and access your personal health records, including medical history, prescriptions, and test results, all within the app.
QR Code Ticketing: Generate QR Code Tickets for booked schedules (in case of a physical visit to a hospital).
Timers & Reminders: Set reminders for medication schedules.
Health Tips and Resources: Receive health tips and resources.
PWA: No need to install from Appstore or Playstore.
Tech stack used:
Next.js: For Frontend.
Appwrite: For Backend.
Zustand: For state management.
AntDesign: For UI components (when necessary).
TailwindCSS: For easy responsiveness.
JavaScript: For writing functions.
Jest: For Testing.
Docker: For Containerization.
Table of Contents:
Prerequisites
Setting up a Next.js Project
Creating an Appwrite Account
Installing and Configuring Appwrite SDK
Implementing User Authentication
Managing Database Collections and Documents
Uploading and Managing Files
Conclusion
Prerequisites:
Before we begin, ensure that you have the following prerequisites:
Basic knowledge of JavaScript and Next.js.
Node.js and npm (Node Package Manager) are installed on your system.
An Appwrite account (sign up at appwrite.io). If you don't have this, I will walk you through creating one in this article.
An integrated development environment (IDE) of your choice. I recommend VS Code.
Setting up a Next.js Project:
In this section, we will cover the steps to set up a new Next.js project if you don't have one:
Step 1: Open your terminal or command prompt and navigate to your desired directory or folder where you want to create the project. Then, run the following command:
npx create-next-app learning-appwrite
This command sets up a new Next.js project with the name learning-appwrite
. You can replace it with a suitable name if you like.
Step 2: Navigate to the Project Directory: Once the project is created, navigate to the project directory using the following command:
cd learning-appwrite
If you changed learning-appwrite
to a different name then use that name.
Step 3: To start the Next.js development server and preview your project in your browser, run the following command:
npm run dev
This command will start the server and provide you with a local development URL (usually localhost:3000). Open your web browser and visit the provided URL to see your Next.js project in action. You're free to develop your Next.Js app, deploy, and set up CI/CD with any free hosting platform of your choice (I will not go into details as this is outside the scope of my article).
Creating an Appwrite Account:
Step 1: To create an Appwrite account, visit https://cloud.appwrite.io/register.
Step 2: Fill in the registration form and submit it, or you can choose to sign up with your GitHub account. After your account gets created, you can proceed to log in.
At the time of this writing, you will be redirected to Appwrite's hackathon page(if this happens just click on the "Go to console" button, which should redirect you to your Appwrite console(mine is shown in the image below).
Enter a name for your cloud project, I have chosen to stick with learning-appwrite
. You can change your project ID to a custom one if you wish, or just leave it as it is for Appwrite's more secure, randomly generated ID. You can now click on "Create Project". Please copy your Project ID and keep it safe, you will need it when configuring Appwrite in your Web app.
Step 3: After your project has been created, you will need to add a platform. We will be adding a WebApp platform since we are integrating a Next.Js application with Appwrite. Locate the "Add a Platform" section and click on "Web App". You will then be prompted to register your new web app, which includes providing the domain name for your hosted project. Mine is https://next-gen-patients.vercel.app as well as a complementary https://next-gen-doctors.vercel.app
Installing and Configuring Appwrite SDK:
Step 1: To use Appwrite in your project, you need to install the Appwrite SDK. This SDK allows your frontend code to interact with the Appwrite server and use its Backend service. Go back to your code editor and open up a terminal in the root folder of your project (where your package.json
file is located) and run this code: npm install appwrite
After the Appwrite package has been installed, create a new file where you will configure your Appwrite client. You can name it anything, mine is settings.config.js
In this file, import Client
from "appwrite"
then use the new Client()
constructor to set up your client. This involves passing your appwrite API base URL into the setEndpoint()
function, and your Project ID into the setProject()
function. These functions exist within Appwrite's Client class, so they can be called like this: Client().setEndpoint("URL").setProject("project ID")
import { Client } from "appwrite";
export const client = new Client()
.setEndpoint("https://cloud.appwrite.io/v1") // appwrite API base URL
.setProject("647f4fd254cfr7c3c91z"); // dummy ID string. Use your actual ID
With this, you have configured your Appwrite SDK.
Implementing User Authentication:
Appwrite's Account service allows you to authenticate and manage a user account. You don't have to manually code your auth implementation. Appwrite handles all of that with their Account
service. In the same settings.config.js
file, you can import the Account
service from Appwrite, pass your client into it and export the account service so it becomes available for use in your sign-up and sign-in page(s).
import { Client, Account } from "appwrite"; // NOTE: import Account
export const client = new Client()
.setEndpoint("https://cloud.appwrite.io/v1") // appwrite API base URL
.setProject("647f4fd254cfr7c3c91z"); // dummy ID string. Use your actual ID
export const accountClient = new Account(client) // our Account service
In your sign-up page, import the accountClient
you created above. Also, import ID
from "appwrite
, but this is not compulsory and should only be used if you want Appwrite to generate a secure ID for your new user. In most cases, you can make use of a custom ID.
import React, { useEffect, useState } from "react";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { ID } from "appwrite";
import { accountClient } from "@/appWrite-client/settings.config";
const SignUp = () => {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errorMessage, setErrorMessage] = useState(null);
const router = useRouter();
const handleSignup = (e) => {
e.preventDefault();
setErrorMessage("");
const name = firstName + " " + lastName;
const promise = accountClient.create(ID.unique(), email, password, name);
promise.then(
function (response) {
accountClient.createEmailSession(email, password);
router.push("/home");
console.log(response);
},
function (error) {
console.log(error);
setLoading(false);
setErrorMessage(error.message);
}
);
};
return (
<>
<Head>
<title>Sign-up Page</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<div>
<form
onSubmit={(e) => handleSignup(e)}
>
{* Your form fields here and your submit button *}
</form>
</div>
</>
);
};
export default SignUp;
The main focus above is the handleSignup()
function block. Notice that it's where we are making use of our accountClient
by calling the Appwrite's create()
method which takes in the user's ID, the user's email, and the user's password:
const promise = accountClient.create(ID.unique(), email, password, name);
you can add additional params depending on the fields you have in your signup form just like I added the name
param, which is a string made up of the user's firstName + " " + lastName
input field values.
NOTE: I am using ID.unique()
because I want Appwrite to generate an ID for each user who signs up. You can provide a custom value for the ID if you want, you can even pass in the email as the ID if you wish.
After the request hits the backend, you can grab the response and do whatever you want with it. In my case, I am handling the user log-in directly by creating an email session. Thanks to Appwrite, this is easily done by calling the createEmailSession()
method on our accountClient
and simply pass the user's email and password into it.
After that, I redirect the logged-in user to the home page.
promise.then(
function (response) {
accountClient.createEmailSession(email, password);
router.push("/home");
console.log(response);
},
// handle errors if any
function (error) {
console.log(error);
setLoading(false);
setErrorMessage(error.message);
}
);
Managing Database Collections and Documents:
The Databases
service allows us to create collections of documents, perform queries and filters on the documents we have created, and efficiently manage a wide range of read and write access permissions without writing too much code. Thanks to Appwrite's out-of-the-box methods. In our project, we will create and read documents from our Appwrite database.
Let us walk through creating a document, and getting documents. You have to log in to your Appwrite console and create a new database in your project. when you do, copy the ID and head back to your settings.config.js
file in your IDE and import the Databases service from Appwrite and let it consume your client.
CREATING DOCUMENTS:
import { Client, Account, Databases } from "appwrite"; // NOTE: import Account, Databases
export const client = new Client()
.setEndpoint("https://cloud.appwrite.io/v1") // appwrite API base URL
.setProject("647f4fd254cfr7c3c91z"); // dummy ID string. Use your actual ID
export const accountClient = new Account(client) // our Account service
export const databaseClient = new Databases(client); // our Databases service
In your page where you want to make post requests, simply import your databaseClient and call the createDocument
method on it.
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useState } from "react";
import Head from "next/head";import { useRouter } from "next/router";
import {
accountClient,
databaseClient,
storageClient,
} from "@/appWrite-client/settings.config";
import { ID } from "appwrite";
function Booking() {
const router = useRouter();
const databaseId =
process.env.USERS_DATABASE_ID || process.env.NEXT_PUBLIC_USERS_DATABASE_ID;
const upcomingCollection =
process.env.UPCOMING_APPOINTMENTS_COLLECTION_ID ||
process.env.NEXT_PUBLIC_UPCOMING_APPOINTMENTS_COLLECTION_ID;
const historyCollection =
process.env.APPOINTMENT_HISTORIES_COLLECTION_ID ||
process.env.NEXT_PUBLIC_APPOINTMENT_HISTORIES_COLLECTION_ID;
const bucketID = process.env.BUCKET_ID || process.env.NEXT_PUBLIC_BUCKET_ID;
useEffect(() => {
const userDetails = accountClient.get();
userDetails.then(
function (response) {
setUId(response.email);
},
function (error) {
message.error("Oops you're not logged in :(");
router.push("/login");
return;
}
);
const storage = storageClient.listFiles(bucketID);
storage.then(
function (response) {
setImages(response.files);
},
function (error) {
console.log(error);
}
);
}, []);
const handleSubmit = async (event) => {
event.preventDefault();
if (uId) {
// user is signed in, get their UID(email)
const date = new Date();
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0");
const randomIndex = Math.floor(Math.random() * images.length);
const randomPic = storageClient.getFilePreview(
bucketID,
images[randomIndex].$id
);
const docId = ID.unique();
const appointmentDetails = {
// object to be posted to db
}
try {
await databaseClient.createDocument(
databaseId,
upcomingCollection,
docId,
appointmentDetails
);
await databaseClient.createDocument(
databaseId,
historyCollection,
docId,
appointmentDetails
);
message.success("Appointment successfully created!", 3);
setShowConfirmation(true);
} catch (error) {
message.error("Appointment creation failed, please try again!", 3);
console.log(error.message);
return;
}
} else {
// user is not signed in' redirect to the login page
message.error("Oops you're not logged in :(");
router.push("/login");
return;
}
};
return (
<>
<Head>
<title>Booking | NEXTGEN Patients</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main
className={`flex flex-col justify-center w-screen gap-6 xl:gap-0 p-6 xl:pt-32 px-4 text-white xl:h-screen xl:overflow-hidden ${
showConfirmation
? "h-screen overflow-hidden"
: "min-h-screen overflow-scroll"
}`}
>
<h1 className="text-3xl text-center text-[#2A9988] md:text-4xl xl:text-6xl">
Book an appointment
</h1>
<div className="flex items-center justify-around mb-16">
<form
className="flex flex-col justify-center w-full max-w-xl p-4 py-5 h-fit md:p-8 gap-7 md:gap-10 xl:max-w-fit"
onSubmit={handleSubmit}
>
{* Input fields here *}
</form>
</div>
</main>
</>
);
}
export default Booking;
A couple of things are happening in the code block above but only pay attention to the try-catch block in the handleSubmit function where there is this:
await databaseClient.createDocument(
databaseId,
historyCollection,
docId,
appointmentDetails
);
The createDocument
function takes in your databaseID, collection ID, document ID, and data to be posted. I have my required IDs in my .env
file so I'm accessing them when required using process.env.VARIABLE_NAME || process.env.NEXT_PUBLIC_VARIABLE_NAME
. As for the document ID, once again I am letting Appwrite generate that, and I have it as docId = ID.unique()
.
GETTING DOCUMENTS
Now that we have created at least one document, let us get it from the database. We are still making use of the databaseClient we configured and in the large code block above. To get data from your appwrite database, you have to call the listDocuments method on the databaseClientT:
const handleLoad = async () => {
try {
const upcomingCollectionData = await databaseClient.listDocuments(
databaseId,
upcomingCollection,
[Query.equal("identification", uId)]
);
console.log(upcomingCollectionData);
setUpcomingArray(upcomingCollectionData.documents);
const historyCollectionData = await databaseClient.listDocuments(
databaseId,
historyCollection,
[Query.equal("identification", uId)]
);
console.log(historyCollectionData);
setHistoryArray(historyCollectionData.documents);
} catch (error) {
message.error("Error fetching appointments, please try again!", 3);
console.log(error.message);
}
};
The listDocuments
method takes in your database ID, collection ID, and queries param. The query param is like a filter that gets specific data depending on what you want. If you just want all documents, you can remove the query array entirely.
DELETING DOCUMENTS:
Documents can be deleted, by calling the deleteDoucument
method on our databaseClient
const cancelApp = async (id) => {
try {
await databaseClient.deleteDocument(databaseId, upcomingCollection, id);
message.success("Appointment successfully cancelled!", 3);
handleLoad();
} catch (error) {
message.error("Error canceling appointment!");
console.log(error.message);
}
};
Here we are cancelling an appointment by deleting it from the db. this method takes in the documentID, collectionID, and document ID for the document you want to delete.
Uploading and Managing Files:
You have save files in you apprite db and get them to be used in your app. This tutorial doesn't cover how to upload from your frontend because i manually uploaded the images i need through my appwrite dashboard but this is how i am getting them and using them in my app.
First add a storage client in your settings.config.js
import { Client, Account, Databases, Storage } from "appwrite"; // NOTE: import Account, Databases, and Storage
export const client = new Client()
.setEndpoint("https://cloud.appwrite.io/v1") // appwrite API base URL
.setProject("647f4fd254cfr7c3c91z"); // dummy ID string. Use your actual ID
export const accountClient = new Account(client) // our Account service
export const databaseClient = new Databases(client); // our Databases service
export const storageClient = new Storage(client); // our Storageservice
const bucketID = process.env.BUCKET_ID || process.env.NEXT_PUBLIC_BUCKET_ID;
useEffect(() => {
const userDetails = accountClient.get();
userDetails.then(
function (response) {
setUId(response.email);
},
function (error) {
message.error("Oops you're not logged in :(");
router.push("/login");
return;
}
);
const storage = storageClient.listFiles(bucketID);
storage.then(
function (response) {
setImages(response.files);
},
function (error) {
console.log(error);
}
);
}, []);
In the page where I need the files, i just call the listFiles
method on the storage client and set the images state to the response.data
and i can now use the images in my state.
To learn more about uploading images, visit https://appwrite.io and go to the docs and look for "storage" or directly visit https://appwrite.io/docs/storage
Challenges faced:
While working on the project, we faced some challenges as we are about a week new to Appwrite. There's a chat feature that is part of the app we built.
The thought flow is like this:
// create the messaging database in our console
// create a messagingClient
// subscribe to the messagingClient subscribe(database.ID.collection.ID.documents)
// make the post from the patients App
// get the subscription response in the doctors App when there's a post request made
// do the reverse so patient gets a subscription response when doctor sends a chat.
The implementation wasn't completed because the realtime subscription service wasn't returning any response when messages are sent to the db. This can be due to the following:
Us not setting up the subscription channel properly.
Having the chat feature shared across two separate projects.
The problem remains unsolved and feature remains imcomplete as of this writing. I'll do well to update this post once we get it work. We also welcome comments, suggestions, and contributions from anyone who can help.
Conclusion:
Appwrite offers a comprehensive and easy to use set of secure APIs, tools, and a perosnal management console to facilitate the development of your applications. I recommend using Appwrite but you need to visit their docs to truly understand what exactly each service does and how best to use it. Also i want to thank hashnode for being Hashnode. I mean, look at this article, now available for anyone who wants to try Appwrite.
Also, special thanks to my Team member: Mainasara Olulanke. Bruh, 🤝 thanks! for real.
Here's a link to the github repo for our opensource hackathon project:
https://github.com/chukusuccess/NextGenPatients
Here's a reminder of things you can do with Appwrite, you can integrate user authentication and management, data and file storage, server-side code execution, image manipulation, and other essential functionalities into your apps that would be a pain to manually write.