Angular Universal Rest API Endpoints
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';
};
server.ts
Look for this commented out line:
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
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);
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'
})
);
};
}
So, there are a few key concepts here.
- Use
HttpClient
to get the data. Angular returns this as an observable, so make it a promise. Don't forget to addHttpClientModule
to the imports ofapp.module.ts
. - 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 theDOCUMENT
route. - Add
<h1>{{ data }}</h1>
to yourapp.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.