Angular Universal Rest API Endpoints

Jonathan Gamble - Apr 4 '22 - - Dev Community

If you use NestJS, you know how easy it is to create REST API endpoints. I am personally building an app in SvelteKit that uses them out of necessity. Nust uses them as well. I read an article somewhere talking about "frameworks that support them..." Well, they all support them (okay the main 4), just not out of the box.

What you may not know is that Vercel started make this popular in NextJS due to the File System API. Basically, it builds a serverless function for each Rest endpoint in order to minimize the cold start time for each route.

Vercel told me that I shouldn't deploy an Angular Universal App to Vercel due to the AWS Lambda 50MB limit. Well, it is actually 250 MB, unzipped.

So, I created a way to deploy to Vercel anyway. I'm a rebel.

This post does not take into account serverless functions, but it would be easy to do so in Vercel. Just add a new file to the api folder.

That being said, let's begin.

handler.ts

Create a handler.ts file in your root directory. Here are my example contents. This handles all routes, but you could easily separate them out into different files.

export const handler = (req: any, res: any) => {
  const func = req.params[0];
  let r = 'wrong endpoint';

  if (func === 'me') {
    r = me();
  } else if (func === 'you') {
    r = you();
  }
  res.status(200).json({ r });
};

const me = () => {
  return 'some data from "me" endpoint';
};

const you = () => {
  return 'some data from "you" endpoint';
};
Enter fullscreen mode Exit fullscreen mode

server.ts

Look for this commented out line:

// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
Enter fullscreen mode Exit fullscreen mode

Change it to this:

// remember to import handler at top of page
import { handler } from 'handler';
...

// Example Express Rest API endpoints
server.get('/api/**', handler);
Enter fullscreen mode Exit fullscreen mode

And that's it for the backend!

As easy as that part was, I still believe Angular Universal can simplify these things.

app.component.ts

import { DOCUMENT, isPlatformServer } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { 
 Component, 
 Inject, 
 Optional, 
 PLATFORM_ID
} from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { REQUEST } from '@nguniversal/express-engine/tokens';

declare const Zone: any;

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  title = 'angular-test';

  data!: string;
  baseURL!: string;
  isServer: Boolean;

  constructor(
    @Inject(PLATFORM_ID) platformId: Object,
    @Optional() @Inject(REQUEST) private request: any,
    @Inject(DOCUMENT) private document: Document,
    private http: HttpClient
  ) {
    this.isServer = isPlatformServer(platformId);

    // get base url
    if (this.isServer) {
      this.baseURL = this.request.headers.referer;
    } else {
      this.baseURL = this.document.location.origin + '/';
    }

    // grab data
    this.getData().then((data) => this.data = data.r);
  }

  async getData(): Promise<any> {
    return await firstValueFrom(
      this.http.get(this.baseURL + 'api/me', {
        headers: {
          'Content-Type': 'application/json',
        },
        responseType: 'json'
      })
    );
  };
}
Enter fullscreen mode Exit fullscreen mode

So, there are a few key concepts here.

  1. Use HttpClient to get the data. Angular returns this as an observable, so make it a promise. Don't forget to add HttpClientModule to the imports of app.module.ts.
  2. The server does not know what your base URL is. If you don't care about testing with npm run dev:ssr, you don't need to worry about it, and just use the full url. However, if you want it to work locally and in production, you need to get the correct baseURL. It is passed to the headers in the request object, so we just get it from that object on the server. In the browser, we get it from origin. There are many ways to do this, but I went with the DOCUMENT route.
  3. Add <h1>{{ data }}</h1> to your app.component.html file.

Example

So, here is this masterpiece at hand:

https://angular-endpoint-test.vercel.app/

and of course the Github.

Don't Fetch Twice

There is one more step you should do, which I left out for brevity. Angular is fetching your REST API endpoint two times: once from the server, and once from the browser. This means you get one more read than necessary.

Now the code above does fetch twice, but you could fetch once on the server, populate the DOM, save the data as a JSON string, and re-apply the data to the Incremental DOM.

I already wrote an article on this about passing state from server to browser in Angular.

So, this should be implemented as well.

Happy Angular Universaling,

J

UPDATE: 4/5/22 - I updated my Github and deployment to transfer state correctly so that it only fetches once.

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