DEV Community

Cover image for How to Deploy Angular Universal to Vercel
Jonathan Gamble
Jonathan Gamble

Posted on • Edited on

How to Deploy Angular Universal to Vercel

Update - Angular 17.2 - 3/2/24

While you still can't get Vercel Edge deployments to work until the nodejs dependencies problem is resolved, you can now deploy @angular/ssr to Vercel serverless functions with a few changes:

  1. Create api/index.js file: ```js

export default import('../dist/YOUR-PROJECT/server/server.mjs')
.then(module => module.app());

2\. Add `vercel.json` file:
```json


{
  "version": 2,
  "public": true,
  "name": "YOUR-PROJECT",
  "rewrites": [
    {
      "source": "/(.*)",
      "destination": "/api"
    }
  ],
  "functions": {
    "api/index.js": {
      "includeFiles": "dist/YOUR-PROJECT/**"
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

3. Comment out run() in server.ts as you will be running the server on Vercel functions.

4. If you don't want prerendering:

  • Rename index.html to index1.html, and update the path in angular.json at projects.architect.build.index to src/index1.html.
  • Also set projects.architect.build.options.prerender to false. This will ensure your code is dynamically rendered.

That's it! Thanks to @mickl, @trongthuong96, and @wolfsoko for the updates in the comments for figuring all this out. I have simplified what you said.

Demo: https://angular-vercel-ssr.vercel.app/
GitHub: https://github.com/jdgamble555/angular-vercel-ssr

Angular Test Site


Original Post


After 176 commits, memorizing the Vercel docs, searching through the Nuxt, Sveltekit, Vercel, and Next GitHub packages, browsing stackoverflow, and pulling my hair out, I finally got this thing to work. Then I didn't; then I did; then I simplified it, I found problems, and I came to a general solution.

Thanks to this guy's overly complicated monorepo, I found the missing pieces. It was not easy.

Here is the final working example:

Vercel Solution

Solution

  1. Create a vercel.json file at the root of your Angular Universal project with YOUR_PROJECT_NAME:

vercel.json



{
  "version": 2,
  "public": true,
  "name": "test-universal",
  "rewrites": [
    { "source": "/(.*)", "destination": "/api" }
  ],
  "functions": {
    "api/index.js": {
      "includeFiles": "dist/YOUR_PROJECT_NAME/browser/**"
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

All we are doing is pointing all requests to the api/ folder. You must also select which files to give your script access to with includeFiles.

2. Rename scripts.build to scripts.build-dev in package.json. Vercel runs npm run build automatically, with only access for the browser. We do not need that in this case.

3. Add scripts.vercel-build with the value npm run build:ssr. This is run specifically within the serverless function to give you access to all your files and scripts.

package.json



{
  "name": "test",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build-dev": "ng build",
    "watch": "ng build --watch --configuration development",
    "test": "ng test",
    "dev:ssr": "ng run test:serve-ssr",
    "serve:ssr": "node dist/test/server/main.js",
    "build:ssr": "ng build && ng run test:server",
    "prerender": "ng run test:prerender",
    "vercel-build": "npm run build:ssr"
  },
...


Enter fullscreen mode Exit fullscreen mode

4. Create the file api/index.js. All scripts in the api directory are automatically used as serverless functions.

api/index.js



const server = require('../dist/YOUR_PROJECT_NAME/server/main');

module.exports = server.app();


Enter fullscreen mode Exit fullscreen mode

5. Push to GitHub. An existing Vercel project will automatically deploy, or you can click New Project, and select your GitHub Repository.

That's it!

This took me a week to do, and it is so simple.

Vercel Team, please add this to your existing templates!

Now, I can use Angular and Vercel with their CDN and Edge Functions. There is similar functionality in Google Cloud, just a pain to configure.

FWI - There is a plugin for Netlify if you prefer a different provider. Either way, Angular Universal is now available on all major servers.

Until next time...

J

Update: If you have the Service Worker enabled, it will look like it is only loading the static version. Disable cookies temporarily, and you can see it works as expected.

Note: I should also tell you Vercel's Serverless functions have a 50mb limit. If you have a giant app, this is not the best server. NextJS is built to use Vercel so that each page uses its own Serverless function. I suspect SvelteKit will follow this pattern now that Rich Harris is on board with Vercel. That being said, Svelte is a baby, and I personally hate React. Better use Cloud Run for bigger apps.

Top comments (56)

Collapse
 
mickl profile image
Mick

Hey! Did you try it out with the new Angular 17 yet? They added integrated SSR support to Angular. Now running "ng serve" or "ng build" automatically starts SSR server, thats awesome!

Anyway, Vercel now throws the error:

Cannot find module '../dist/server/main'

The server folder looks way different now and has .mjs modules instead of .js.

Angular 16:
Image description

Angular 17:
Image description

Collapse
 
wolfsoko profile image
Wolfram Sokollek

I came up with this solution for the app/index.js:

async function loadServer() {
  const serverModule = await import('../dist/apps/{your-app}/server/server.mjs');
  return serverModule.app;
}

export default loadServer().then(app => app());
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jdgamble555 profile image
Jonathan Gamble • Edited

Try with this on the import file:

const server = require('../dist/angular-vercel/server/server');

module.exports = server.app();
Enter fullscreen mode Exit fullscreen mode

And just keep all the package.json the same without editing it.

J

Collapse
 
mickl profile image
Mick • Edited

Ahhhh I totally forgot about the api/index.js and was wondering here he gets this server/main from ... :) Anyway I could NOT get it to work yet! I tried to all different kind of imports and renaming the file from index.js to index.mjs but I couldnt find the correct way yet. Maybe you have a look at it?

import server from '../dist/angular-vercel/server/server';

module.exports = server.app();
Enter fullscreen mode Exit fullscreen mode

OR

import app from '../dist/server/server.mjs';

module.exports = app();
Enter fullscreen mode Exit fullscreen mode

Nothing works...

Btw the package.json is now significantly different! Thats because ng build already builds browser and server app, so no more need for build:ssr! This also means build/deploy time went from 2 minutes to 1 minute :)

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble

You have to use require because it is JS. Did you try my code? You're importing 'server.mjs'... hence server/server.

Thread Thread
 
mickl profile image
Mick

I think thats the first thing tried but in mjs there is no require allowed anymore when I remember correct. I renamed index.js to index.mjs because it was not working.

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble
Thread Thread
 
mickl profile image
Mick • Edited

I got it exactly like you, I double checked everything, I dont know what I am missing. Error:

Cannot find module '../dist/server/server'
Require stack:

  • /var/task/api/index.js Did you forget to add it to "dependencies" in package.json?

My outputPath is just dist.

Are you on Node 20 maybe? Because I cant use Node 20 because the version they use is not compatible with @angular/animations:

error @angular/animations@17.0.5: The engine "node" is incompatible with this module. Expected version "^18.13.0 || >=20.9.0". Got "20.5.1"

EDIT: Nope, same error with Node 20 when I add --ignore-engines flag to yarn.

Thread Thread
 
mickl profile image
Mick

Found it! The outputPath in angular.json actually need to be dist/something and cant be just dist. I dont know why but maybe it has something todo with how Vercel deploys the files.

Doesnt work:

dist/
  - server/
  - browser/
Enter fullscreen mode Exit fullscreen mode

Works:

dist/
  - something/
       - server/
       - browser/
Enter fullscreen mode Exit fullscreen mode

Thats an important info! Will you update the article to Angular 17 or write a new one?

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble

I didn't change any settings. It defaults to Node 18. I also didn't edit package.json at all. Make sure you're using js and not ts. You can leave me your repo and I can look.

Thread Thread
 
mickl profile image
Mick • Edited

I am pretty sure that your example does not work, too:

angular-ssr-vercel-hazel.vercel.ap...

-> Your function is crashing. Probably with the same error as mine:

Error [ERR_REQUIRE_ESM]: require() of ES Module /var/task/dist/yo/server/server.mjs not supported.

And the reason that you see a website on your example is:

  1. The page (index.html) is a built-time prerendered static html page
  2. The request does NOT go through your serverless function
  3. You can confirm this by setting prerender to false in you angular.json -> You will now see that the normal browser website is served and your ssr serverless function never runs.

If you want we can take a look together but how can I contact you? Or maybe you already find the reason why...

Btw I also see Vercel compiling my index.js:

Warning: Node.js functions are compiled from ESM to CommonJS. If this is not intended, add "type": "module" to your package.json file.
Compiling "index.js" from ESM to CommonJS...

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble

You're right, but its not detecting my vercel.json file at all! Not sure why. It just loads the regular browser files and never runs the serverless function, and no errors.

Thread Thread
 
mickl profile image
Mick

Did you find anything out in the meantime?

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble

@mickl @trongthuong96 @wolfsoko - Hey guys, I got it working thanks to all of your ideas. See the updated post with a demo.

Collapse
 
mickl profile image
Mick • Edited

Angular 17

A lot has changed :) Build times went down from 2:30 min to 1:00 min!

Please note that (according to my findings) it does NOT work if you set outputPath in angular.json just to dist. It needs to be dist/my-project, otherwise it does not work for some reason.

package.json: No need to change anything, SSR is already included in Angular. If you are upgrading from Angular < 17 you can remove the :ssr scripts and the vercel-build script:

"start": "ng serve",
"build": "ng build",
Enter fullscreen mode Exit fullscreen mode

vercel.json:

{
  "version": 2,
  "public": true,
  "name": "my-project",
  "rewrites": [
    {
      "source": "/(.*)",
      "destination": "/api"
    }
  ],
  "functions": {
    "api/index.js": {
      "includeFiles": "dist/my-project/**"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

api/index.js

const server = import('../dist/my-project/server/server.mjs');

module.exports = server.app;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jdgamble555 profile image
Jonathan Gamble

What does your package.json look like?

Thread Thread
 
mickl profile image
Mick

You dont change anything in Angular 17 SSR is already integrated:

"start": "ng serve",
"build": "ng build",

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble • Edited

My function doesn't seem to be working, but I think Angular now works with ssr out of the box so it may be a moot point !? - angular-vercel-ssr.vercel.app/api

angular-vercel-ssr.vercel.app/

Thread Thread
 
mickl profile image
Mick

If you upgraded your project then use the angular.json from a fresh Angular 17 project because a lot has changed there!

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble

Do you have a working link where the "api" folder that holds the function also works? I created a new project for testing. It seems to work out of the box (with no changes at all), but I think my function is not working. Is your function working when you navigate to /api?

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble

I was able to get it to work both ways api now forwarding to regular. See article changes for how. However, I think it may now work out-of-the-box if we don't do any changes at all.

Thread Thread
 
mickl profile image
Mick

Yes it works fine. It redirects me to "/". If I disable JavaScript I see the prerendered pages. I see in you example you export server.app() while it should be server.app. Maybe thats the cause?

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble

I'm using a module to import it and it works. See the top of the article for the updates. Also, again, I think Angular SSR may work with Vercel now out of the box and none of this is necessary.

Thread Thread
 
mickl profile image
Mick

Why do you think it works out of the box? I tried removing vercel.json and api folder but it just shows 404 because build is now creating browser and server folder). If I go to /browser it displays the static index.html (without going through server first) and fails to load all the scripts.

Thread Thread
 
mickl profile image
Mick

@jdgamble555 I tried your code:

import * as server from '../dist/angular-vercel-ssr/server/server.mjs';

export default server.app();
Enter fullscreen mode Exit fullscreen mode

But for me it only works like this:

const server = import('../dist/angular-vercel-ssr/server/server.mjs');

module.exports = server.app;
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
jdgamble555 profile image
Jonathan Gamble

Did you add "type": "module" to package.json?

Collapse
 
trongthuong96 profile image
Thuong

Angular 17: file api/index.js:
Image description

Vercel.json:
Image description

Render on the home page:
Image description

Because by default the home page is not rendered. Don't know why { "source": "/(.*)", "destination": "/api" } doesn't automatically redirect to the home page.

(I used google translate)

Collapse
 
mickl profile image
Mick • Edited

It kinda works but for all static files (javascript, css, images) it returns the prerendered html page again. Does ist work for you?

It works with: "includeFiles": "dist/MYPROJECT/**

Thread Thread
 
trongthuong96 profile image
Thuong

I found a solution to work without redirecting the home page. Just rename index.html in src to index1.html. And in angular.json "index": "src/index1.html".

Thread Thread
 
mickl profile image
Mick

I dont have this problem! Just check my example and use the angular.json from a fresh Angular 17 project.

Thread Thread
 
rayhan_nj profile image
Raihan Nismara

is it working now ssr on vercel?

Thread Thread
 
mickl profile image
Mick

Yes works fine!

Collapse
 
leehodges profile image
Lee Hodges • Edited

Thanks for this! I made a few changes to my setup so I'm not always running production environments when I want to hit say staging backend.

Strip out "vercel-build": "npm run build:ssr" from package.json
Update "build:ssr" to "ng build --configuration production && ng run project-name-here:server"

Create vercel.sh in your root directory of the project

if [[ $VERCEL_GIT_COMMIT_REF == "main" ]]; then
  echo "This is our production branch"
  npm run build:ssr
elif [[ $VERCEL_GIT_COMMIT_REF == "master" ]]; then
  echo "This is our production branch"
  npm run build:ssr
else
  echo "This is not our production branch"
  npm run build:staging-ssr
fi 
Enter fullscreen mode Exit fullscreen mode

The above is just looking at what branch is being deployed, if it's main/master build:ssr will run if its any other branch my staging-environment deployment script will run.

Settings on Vercel for the project add custom build command of sh vercel.sh
settings

Collapse
 
amaurygoncalves profile image
Amaury Gonçalves Costa • Edited

Thanks man for this!! It has saved me, rsss. Thanks 4 share!

Collapse
 
mickl profile image
Mick

Thanks a lot for the article! Does this also work with Vercel Edge Runtime, which is just a subset of the Node API?

Collapse
 
jdgamble555 profile image
Jonathan Gamble • Edited

I have not tested it, but there is no reason it shouldn't work on the edge, as it is just JavaScript. You can try adding this to the index.js:

export const config = {
  runtime: 'edge',
};
Enter fullscreen mode Exit fullscreen mode

vercel.com/docs/edge-network/caching

Let me know if you figure it out!

J

Collapse
 
mickl profile image
Mick • Edited

It would require to also set the response header as written in the docs BUT I tried it out and it doesnt work:

Error: The Edge Function "api/index" is referencing unsupported modules:

  • dist/server/main.js: crypto, fs, http, https, net, os, path, querystring, stream, string_decoder, timers, tty, url, zlib, node:fs, node:path

There is an open feature request to remove the dependencies of Angular Universal (e.g. remove Express) so it can work on workers that dont support full Node API like Vercel Edge Functions or Cloudflare Pages BUT it will probably be closed if it doesnt get 20 upvotes in the next week:

github.com/angular/angular-cli/iss...

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble • Edited

Yup, I just tried it and go the same error. Normally in Node.js, you could use the "browser" key in package.json to ignore certain imports, but the edge uses Deno. Until this gets resolved (I upvoted BTW), I don't see Angular Universal being possible on Vercel or any Edge. However, AnalogJS may be a different thing.

Thread Thread
 
mickl profile image
Mick

Did you find anything out in the meantime?

Thread Thread
 
mickl profile image
Mick

I got it working thangs to @trongthuong96 ! I posted it on top level

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble

Ok, great, could you post the working config?

Thread Thread
 
mickl profile image
Mick

Posted it on top level

Collapse
 
cotentin profile image
Corentin

Hi, I'm trying to push my Angular app (v16.2) to Vercel with SSR. I followed the steps and the deployment works fine. However, when I inspect the page source, I only see an <app-root> tag with nothing inside, indicating that the site isn't rendered server-side. I'm having trouble understanding why this is happening.

Collapse
 
jdgamble555 profile image
Jonathan Gamble

Did you follow the index renaming and prerendering off part as well? I have also only tested on Angular 17.

Collapse
 
meabhinavakhil profile image
Abhinav • Edited

Hey, this is my configuration but i am getting errors as show below.

Image description

api/index.js

Image description

vercel.json

Image description

package.json

Image description

Image description

using Angular V17

Please let me know on how to fix this. Thanks

Collapse
 
panesarpbx8 profile image
Sukhpreet Singh

Thanks for sharing this man! this is helpful

but one problem I am facing is my app is not server-rendered.

the home url is working but only the static version (with cookies disabled)
but any other url is taking more than 10 seconds to respond, resulting in 504 function timeout error.

could you please help my out?
Thanks

Collapse
 
jdgamble555 profile image
Jonathan Gamble

You may have a loop in your code. Make sure to test it with npm run dev:ssr first. Usually it is a login code that should only be ran on the frontend.

Collapse
 
georgeknap profile image
George Knap

why is your functions pointing to dist/YOUR_PROJECT_NAME/browser/** ?
Shouldn't it be from .../server/**?

Collapse
 
jdgamble555 profile image
Jonathan Gamble

No, the server is the index.js file. That is for the static files like images, css, etc

Collapse
 
klalib_aek profile image
Klalib

Thanks for this, How can i used with i18n ?

Collapse
 
jdgamble555 profile image
Jonathan Gamble

Not sure about that, never used i18n on any framework. Post here if you figure it out!

Collapse
 
hieutranagi47 profile image
Hieu Tran

I do like this:
api/en/index.js

export default import('../../dist/langs/server/en/server.mjs')
  .then(module => module.app());
Enter fullscreen mode Exit fullscreen mode

api/vi/index.js

export default import('../../dist/langs/server/vi/server.mjs')
  .then(module => module.app());
Enter fullscreen mode Exit fullscreen mode

vercel.json:

{
    "version": 2,
    "public": true,
    "name": "langs",
    "rewrites": [
      {
        "source": "/vi/(.*)",
        "destination": "/api/vi"
      },
      {
        "source": "/en/(.*)",
        "destination": "/api/en"
      }
    ],
    "functions": {
      "api/en/index.js": {
        "includeFiles": "dist/langs/en/**"
      },
      "api/vi/index.js": {
        "includeFiles": "dist/langs/vi/**"
      }
    },
    "redirects": [
      { "source": "/", "destination": "/en", "permanent": false }
    ]
  }
Enter fullscreen mode Exit fullscreen mode

And angular.json

"projects": {
    "langs": {
      ...
      "i18n": {
        "locales": {
          "vi": {
            "translation": "src/locales/messages.vi.xlf",
            "baseHref": "/vi/"
          },
          "en": {
            "translation": "src/locales/messages.xlf",
            "baseHref": "/en/"
          }
        }
      }
...
  }
}
Enter fullscreen mode Exit fullscreen mode

I don't know how to make it work with the default language (en) to use "baseHref": "/"

Collapse
 
mickl profile image
Mick • Edited

Thanks for the article!! One little thing: Instead of step 2 and 3 you could just go to the Vercel website and tell it to run "npm run build:ssr" or "yarn build:ssr".

Collapse
 
jdgamble555 profile image
Jonathan Gamble

Well you have to have the Vercel config file anyway, so this actually skips that step.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.