Appwrite with Angular SSR

Thanks to @Caryntjen from Discord for bringing this problem up

Table Of Contents

Introduction

Server-side rendering can help your website speed up the initial load and let bots access your dynamic data to improve SEO. This article will show you how to quickly solve a problem with Appwrite data not being load before rendering a page server-side.

To solve our problem, we will use the library angular-zen. This library will create a zone.js task under the hood, and that helps Angular Universal understand your async code. To learn more about this, you can visit their docs: Angular zen docs

In SSR, the server doesn't wait for the async code to complete. The result is scrapers and search engines receiving a page without resolved data, which is bad in case you need them to read some resolved metadata tags for example. Use resolveInMacroTask() to have your server block and wait for resolves before rendering.

Setup Angular project with Appwrite

Before solving the problem, let's see the problem! We start by creating an empty angular project:

ng new appwrite-ssr
cd appwrite-ssr
? Do you want to enforce stricter type checking and stricter bundle budgets in the workspace?
  This setting helps improve maintainability and catch bugs ahead of time.
  For more information, see https://angular.io/strict Yes
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? CSS

Make sure to pick the same options to have the same results

Now, let's write some Appwrite code. To use Appwrite in our frontend project, we need to install its client-side SDK:

npm i appwrite

This is a simple javascript/typescript library with no connection to Angular, so we don't need to worry about importing modules or injecting services. For simplicity, we will do everything in our app.component. Still, it is strongly recommended to put all Appwrite logic into a separate appwrite.service to share data across multiple components in an actual project easily.

Our app.component.ts should look like this:

import { Component, OnInit } from '@angular/core';
import { Appwrite } from 'appwrite';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  title = 'appwrite-ssr';
  currencies: any; // Do not use any in real project

  async ngOnInit() {
    let sdk = new Appwrite();

    sdk
      .setEndpoint('https://server.matejbaco.eu/v1') // Your API Endpoint
      .setProject('60f2fb6e92712'); // Your project ID

    // Load currencies from appwrite
    const appwriteCurrencies = await sdk.locale.getCurrencies();

    // Store the result into component variable for use in HTML code
    this.currencies = appwriteCurrencies;
  }
}

First, we imported Appwrite SDK using import { Appwrite } from 'appwrite';. Then, inside ngOnInit we initialized a new instance of the SDK that is connected to our Appwrite server. Finally, we load a list of currencies from Appwrite and store it into a variable to use in HTML code.

Let's switch to app.component.html. This is our code:

<h1>Total currencies:</h1>

<!-- We don't have data yet, loading... -->
<p *ngIf="!currencies">...</p>

<!-- Data loaded, let's count them -->
<p *ngIf="currencies">Total: {{ currencies.sum }}</p>

We simply write two blocks of code - one for when data is not loaded yet, one after the loading is finished. Now, if we run ng serve and visit http://localhost:4200/, we can see the currencies being loaded successfully:

What about server-side rendering? Let's see... If we look at the source code of our application, we can this:

No useful data for bots! Let's fix that.

Add Angular Universal to our project

To prepare our project for server-side rendering, we need to add a new Angular library. Let's stop our Angular development server and run ng add @nguniversal/express-engine. Then, we can run npm run dev:ssr to have the same development server running, but this time with server-side rendering. Let's see what our website looks like to bots now:

This is awesome, one step at a time! Our Angular code is being rendered properly because we can see our title Total currencies:. We are not done yet, because this pre-rendered HTML does not include our Appwrite data. Instead, we can see ....

Connect Appwrite to Angular Universal

As mentioned initially, we will use a library that will help us run the task server-side. To do this, we stop our development server and run npm i @bespunky/angular-zen. Once the library is installed, let's start the development server with npm run dev:ssr.

Angular zen is an Angular library, so we need to add it into imports of our module for it to work properly. To do this, we go into app.module.ts and add add RouterXModule as an import. The module should look like this:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterXModule } from '@bespunky/angular-zen/router-x';
import { AppRoutingModule } from './app-routing.module';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
    AppRoutingModule,
    RouterXModule.forRoot(),
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

My IDE noticed the error Appears in the NgModule.imports of AppServerModule, but itself has errors, but I ignored it, and it got resolved by Angular re-building the app. If you can see this error, simply restart the development server, and you should be good to go.

We need to use RouteAware class in our app.component.ts because we need to access its resolveInMacroTask() method. To do that, we can make our component extend RouteAware. Then we wrap our async code in ngOnInit into resolveInMacroTask and await its result as a promise. Our code will look like this:

import { Component, OnInit } from '@angular/core';
import { RouteAware } from '@bespunky/angular-zen/router-x';
import { Appwrite } from 'appwrite';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent extends RouteAware implements OnInit {
  title = 'appwrite-ssr';
  currencies: any; // Do not use any in real project

  async ngOnInit() {
    let sdk = new Appwrite();

    sdk
      .setEndpoint('https://server.matejbaco.eu/v1') // Your API Endpoint
      .setProject('60f2fb6e92712'); // Your project ID

    await this.resolveInMacroTask(async () => {
      // Load currencies from appwrite
      const appwriteCurrencies = await sdk.locale.getCurrencies();

      // Store the result into component variable for use in HTML code
      this.currencies = appwriteCurrencies;
    }).toPromise();
  }
}

We are good to go! Let's see it in action. If I visit our page, I can see the data:

If I look at the source code of pre-render, I can see the data too!

That's it! I hope this article helped you to use Appwrite with Angular Universal. If you have any questions, feel free to join Appwrite's Discord server and chat with their amazing community: https://appwrite.io/discord

31