Full Stack Application with Bun, Appwrite And NextJS

You might have heard of Bun and Appwrite recently.

Bun is a new, faster alternative to NodeJS and NPM.
And Appwrite just launched their Cloud Version, which is a cool and easy-to-configure alternative to Firebase.

So, let's look at how to build a really fast, modern full stack application with :

NextJS, Bun, Appwrite And TailwindCSS

I had built this typing practice website earlier : Typr

Typr Web

It's been 2 years, and it's time to give it an upgrade!!

Typr - Modern Typing Practice built with the power of Appwrite

The first version above, did not have any way to track your progress based on an account. And of course, at that time Neomorphic designs were the trend. And I built it using HTML, CSS and Vanilla Javascript.

But the Typr 2.0 has all the features that were missing earlier.


And how did I build it?

Continue below to find out!! 🤯

Using Bun to create NextJS App

Yes, you read that right! The create-next-app works with Bun. Just use the --use-bun flag :

npx create-next-app@latest typr-next --use-bun
Enter fullscreen mode Exit fullscreen mode

And you use the blazing fast speeds of bun ⚡️.

Choose the options according to you :

You will see a bun.lockb file which is used to store the packages details and their metadata so that when the repository is cloned, the dependencies are installed faster.

The Main Pages

Since I am creating a typing practice website, the most important page is the practice page itself. Naturally, I created it first.

I used API Ninjas to get a few endpoints :

  • Quotes
  • Facts
  • Jokes To create random texts from the real world so users can get used to typing real words and sentences.

A really important feature in the typing page is updating UI as the user types to display what he has typed till now.

For this I created a separate component and passed in 2 parameters :

  • Typed Length : This sets the length of the text that needs to be highlighted.
  • Original : This takes in the original string that is being typed.

I divide the original into 2 parts : TypedString and UntypedString and display them in different colors :


export default function TyprDisplay({
    text,
    typedLength
}) {
    if (typeof text !== "string") return null;
    const activeText = text.substring(0, typedLength);
    const untyped = text.substring(typedLength);
    return (
        <div className='w-full'>
            <span className='text-white'>{activeText}</span>
            <span className='text-zinc-500'>{untyped}</span>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

And the practice page has some more components like : errors, typed(which is the typed string), score calculator, etc.

Check out the code here(fairly simple to understand).

Appwrite Setup

I know most of you have been waiting for this. So let's dive into Setting up Appwrite Cloud for the App.

1. Head on to : Appwrite Cloud ☁️ and create your account.

Once you have created your account and logged in, you will see a page like this :

Appwrite Cloud Setup

Note : You won't see projects, you will see the create project button in the top right corner.

2. Click on the Create Project button

Select the Empty Project option and give a name.

In the next screen, you will see platforms that you would like to create for :

Appwrite Cloud Project Setup

Once you select the platform, follow the guide Appwrite provides for that platform.

3. Hostname and Installation 🛠️

The Setup Guide has the first page as below :

Installing Appwrite in NodeJS

The important thing to note here is : The Hostname during development should be localhost and for different domains, only add the path.domain without the https or subdmains

To install in your Web Projects (ReactJS, NodeJS, NextJS) :

bun install appwrite 
Enter fullscreen mode Exit fullscreen mode

The Setup is now complete and you can use Appwrite cloud in your Project.

Using Appwrite for Authentication 🔐

For using Appwrite Cloud in your project, you will need 2 things :

  • Project ID, which is displayed in your project dashboard page :

Appwrite Project ID

  • Appwrite Cloud Endpoint, which is currently the following :
https://cloud.appwrite.io/v1
Enter fullscreen mode Exit fullscreen mode

You can also find both of them in the project settings tab :

Appwrite Cloud API Endpoint

Once you have these, create a .env file and store them there.

For doing any task with Appwrite Cloud, you need to connect to the client. For this use the following code (Note here that I have abstracted this get client method as this is required in multiple places) :

import { Client } from "appwrite";

export const getAppwriteClient = () => {
    const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID;
    const client = new Client()
    .setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
    .setProject(PROJECT_ID);               // Your project ID
    return client;
}

Enter fullscreen mode Exit fullscreen mode

Now, you can call this function whenever you want to use Appwrite.

For my app, I mainly used 4 Authentication features :

  • Creating a User
  • Creating a User Session
  • Getting the current logged in User
  • Deleting current Session

For this the default selections as shown below are enough(the settings tab of the Auth section in the Cloud Dashboard), but Appwrite supports many other OAuth providers that you can explore.

Appwrite Cloud OAuth Providers

Creating a User or Signing Up

Creating a User is the first step for Authentication, also called Sign Up.
Appwrite Cloud supports Email and Password based Users.

For creating a user, we use the account.create() method.

Note here, that we need the Appwrite client(which we will get from the method we created earlier) and then pass in the client created to Account constructor provided by Appwrite.

We can then use this object to create a new user with the following parameters :

  • Unique ID which can be created using the ID.unqiue() method provided by Appwrite.
  • Email
  • Password
  • Name

The following function takes care of this :

export const createUser = async (data) => {
    const {email,
        password,
        name} = data;
    try {
        const client = getAppwriteClient();
        const account = new Account(client);
        if(!email || !password || !name){
            return {
                status : 400,
                message : "Email/Password/Name missing",
                data : null,
                error : new Error("Credentials Missing")
            }
        }
        const result = await account.create(
            ID.unique(),
            email,
            password,
            name
        );
        return {
            status : 201,
            message : "Account Created",
            data : result,
            error : null
        };
    } catch (error) {
        console.log(error);
        if(error.name && error.name==='AppwriteException'){
            return {
                status : error.code,
                message : error.response.message,
                data : null,
                error
            }
        }
        return {
            status : 500,
            message : "Account not created",
            data : null,
            error
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating user Session or Logging In

Once a user is created, you need to create a new session for the user to login. Similar to the above, we use the createEmailSession method for this :

const client = getAppwriteClient();
        const account = new Account(client);
        if(!email || !password){
            return {
                status : 400,
                message : "Email/Password missing",
                data : null,
                error : new Error("Credentials Missing")
            }
        }
        const result = await account.createEmailSession(
            email,
            password,
        );
Enter fullscreen mode Exit fullscreen mode

Getting Current Logged In User

An important thing is that we want to check if there is a user currently logged in.

For this we can use the account.get() method instead of asking the user to login every time they open the website.

const client = getAppwriteClient();
        const account = new Account(client);
        const {name, email, $id} = await account.get(); 
Enter fullscreen mode Exit fullscreen mode

NOTE : If there is no user currently logged in, this method returns an Appwrite Exception which can be handled in the catch block.

Deleting User Session or Logging Out

An important step is logging out a user once they are done to protect their data. This is also relatively simple and takes place in 2 steps :

  • Getting the current SessionID
  • And, Deleting the session
const client = getAppwriteClient();
        const account = new Account(client);
        const session = await account.getSession('current') 
        const sessionId = session?.$id;
        const response = await account.deleteSession(sessionId);
Enter fullscreen mode Exit fullscreen mode

And their you have it : the complete authentication flow for your website that can be setup in minutes with the user of Appwrite Cloud.

The Authentication flow looks like :

Appwrite Authentication

Using Appwrite for Data Management

Appwrite Cloud also provides the functionality to create databases and store data based on our needs.

You can watch the video below or follow the steps below that(Both are same)

Database Setup

Just like the project setup, database setup is also super easy.

1. Create a Database

Before adding data, you need to create a Database for your project.

Head to the Database section and click on Create Database button. Add a name and (optionally) add a custom ID.

2. Create a Collection

Similar to the above, click on the Create Collection button once your database is created. You also have the option to create a custom ID.

3. Creating Attributes 📌

This is a really important step. Without creating the attributes, you will NOT ❌ be able to add new Documents.

You have a few options to select from for your new attributes like :

  • Email
  • URL
  • String
  • Enum
  • Even Relationships(how cool is that!!) 🤯

    awesome appwrite

Working with Data

You have setup your database and created you collection.

Now let's get to the actual fun part. You will see how easy it is to add and retrieve data from Appwrite Cloud Database.

Step 1 : Adding the `Database and Collection IDs` to `env` Before accessing the Database, you need to get the Database ID and Collection ID from the Dashboard. It is displayed right beside the Database Name and Collection Name : ![Appwrite Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ya5s0c799nyfuoncy810.png)

Step 2 : Connecting to the Database

Appwrite provides a class called Databases that helps us connect with the Database. But, we need to pass in the client here are well.

const client = getAppwriteClient();
        const databases = new Databases(client);
Enter fullscreen mode Exit fullscreen mode

Step 3 : Creating Documents

After connecting to the Database client and before reading data, we need to create Data.

For this, Appwrite provides us the databases.createDocument method that takes in 4 arguments by default :

  • DatabaseID 👉🏻 That we saved in the env file.
  • CollectionID 👉🏻 That we saved in the env file.
  • UniqueID 👉🏻 That helps identify each document, again ID.unique() can be used here.
  • Document 👉🏻 Javascript object that contains the actual data.

The complete code for the above looks like this :

const client = getAppwriteClient();
        const databases = new Databases(client);
        const databaseID = process.env.NEXT_PUBLIC_DATABASE_ID;
        const collectionID = process.env.NEXT_PUBLIC_COLLECTION_ID;
        const userId = (await getCurrentUser()).data.userId;
        const doc = await databases.createDocument(databaseID, collectionID, ID.unique(), {
            ...submission,
            owner : userId
        }, 
            [
                Permission.read(Role.user(userId)),
                Permission.update(Role.user(userId)),
            ]
        );
// way to check if submission was successfull
        if(!doc["$id"]){
            throw new Error("Submission Failed");
        }
Enter fullscreen mode Exit fullscreen mode

📌 NOTE : We are adding Permissions here. along with the document. It is optional and when you don't provide any, the default permissions allow anybody to read the document, but only the user who created it to modify or delete it.

You can read more about Appwrite Permissions here e🔗

Step 4 : Fetching Data

Once you have created data, you can start fetching them.

By default, you do not have any method to fetch only the current user's data. But you can use Queries to modify your results.

For my app, I only need to fetch the current user's data so I used the following query :

[Query.equal("owner", [userId])]
Enter fullscreen mode Exit fullscreen mode

You can read more about Appwrite Queries here 🔗

Also, the complete function to fetch the user's data is :

const client = getAppwriteClient();
        const userId = (await getCurrentUser()).data.userId;
        const databases = new Databases(client);
        const databaseID = process.env.NEXT_PUBLIC_DATABASE_ID;
        const collectionID = process.env.NEXT_PUBLIC_COLLECTION_ID;
        const submissions = await databases.listDocuments(databaseID, collectionID,[Query.equal("owner", [userId])],);
        return {
            status : 200,
            message : "Fetched Submissions",
            submissions,
            error : null
        }
Enter fullscreen mode Exit fullscreen mode

And there you have a full stack web app created easily with the power of Appwrite cloud.

Pricing Options 💰

Currently Appwrite is in beta, but they are soon launching their Pro and Enterprise plans 👇🏻

Appwrite Pricing

A huge shoutout to the Appwrite team for building such an awesome Open-Source project.

You can also give me FOLLOW for more blogs like this!!