AWS lambda implementation

I’m attempting to convert an Angular application to Angular Universal for SSR purposes, which was considerably easy to manage. I’ve followed multiple guides found on the web in a vain attempt to then deploy the application to AWS. Put simply, serverless itself just doesn’t work. Take this guide as a prime example and to avoid too much bloat going over all of the variations I’ve tried, I’ll simplify this post with only details from this version.

I’ve basically copy/pasted everything recommended in the blog post. This is my lambda.js:

const awsServerlessExpress = require("aws-serverless-express");
const server = require("./dist/app-name/serverless/main");
const awsServerlessExpressMiddleware = require("aws-serverless-express/middleware");

const binaryMimeTypes = [
  "application/javascript",
  "application/json",
  "application/octet-stream",
  "application/xml",
  "image/jpeg",
  "image/png",
  "image/gif",
  "text/comma-separated-values",
  "text/css",
  "text/html",
  "text/javascript",
  "text/plain",
  "text/text",
  "text/xml",
  "image/x-icon",
  "image/svg+xml",
  "application/x-font-ttf",
];

server.app.use(awsServerlessExpressMiddleware.eventContext());
const serverProxy = awsServerlessExpress.createServer(
  server.app,
  null,
  binaryMimeTypes
);
module.exports.handler = (event, context) =>
  awsServerlessExpress.proxy(serverProxy, event, context);

The tsconfig.serverless.json:

{
  "extends": "./tsconfig.app.json",
  "compilerOptions": {
    "outDir": "./out-tsc/serverless",
    "target": "es2019",
    "types": [
      "node"
    ]
  },
  "files": [
    "src/main.server.ts",
    "serverless.ts"
  ]
}

The serverless.ts:

import 'zone.js/dist/zone-node';

import {APP_BASE_HREF} from '@angular/common';
import {ngExpressEngine} from '@nguniversal/express-engine';
import * as express from 'express';
import {existsSync} from 'fs';
import {join} from 'path';

import {AppServerModule} from './src/main.server';

// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
  const server = express();
  const distFolder = join(process.cwd(), 'dist/app-name/browser');
  const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';

  // Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
  server.engine('html', ngExpressEngine({
    bootstrap: AppServerModule,
  }));

  server.set('view engine', 'html');
  server.set('views', distFolder);

  // Example Express Rest API endpoints
  // server.get('/api/**', (req, res) => { });
  // Serve static files from /browser
  server.get('*.*', express.static(distFolder, {
    maxAge: '1y'
  }));

  // All regular routes use the Universal engine
  server.get('*', (req, res) => {
    res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
  });

  return server;
}

export * from './src/main.server';

The serverless.yml:

service: app-name
plugins:
  - serverless-apigw-binary
  - serverless-offline
provider:
 name: aws
 runtime: nodejs12.x
 memorySize: 192
 timeout: 10
package:
 exclude:
    - ./**
 include:
    - "node_modules/aws-serverless-express/**"
    - "node_modules/binary-case/**"
    - "node_modules/type-is/**"
    - "node_modules/media-typer/**"
    - "node_modules/mime-types/**"
    - "node_modules/mime-db/**"
    - "dist/**"
    - "lambda.js"
custom:
 apigwBinary:
 types:
      - "*/*"
functions:
 api:
 handler: lambda.handler
 events:
      - http: GET {proxy+}
      - http: GET /

And the package json:

{
  "name": "app-name",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build --configuration production",
    "watch": "ng build --watch --configuration development",
    "test": "ng test",
    "dev:ssr": "ng run app-name:serve-ssr",
    "serve:ssr": "node dist/app-name/server/main.js",
    "build:ssr": "ng build && ng run app-name:server",
    "prerender": "ng run app-name:prerender",
    "serve:serverless": "serverless offline start",
    "build:serverless": "ng build --configuration production && ng run app-name:serverless:production"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^14.0.0",
    "@angular/common": "^14.0.0",
    "@angular/compiler": "^14.0.0",
    "@angular/core": "^14.0.0",
    "@angular/forms": "^14.0.0",
    "@angular/platform-browser": "^14.0.0",
    "@angular/platform-browser-dynamic": "^14.0.0",
    "@angular/platform-server": "^14.0.0",
    "@angular/router": "^14.0.0",
    "@nguniversal/express-engine": "^14.2.0",
    "aws-serverless-express": "^3.4.0",
    "express": "^4.15.2",
    "rxjs": "~7.5.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.11.4"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^14.0.2",
    "@angular/cli": "~14.0.2",
    "@angular/compiler-cli": "^14.0.0",
    "@nguniversal/builders": "^14.2.0",
    "@types/express": "^4.17.0",
    "@types/jasmine": "~4.0.0",
    "@types/node": "^14.15.0",
    "jasmine-core": "~4.1.0",
    "karma": "~6.3.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage": "~2.2.0",
    "karma-jasmine": "~5.0.0",
    "karma-jasmine-html-reporter": "~1.7.0",
    "serverless": "^3.23.0",
    "serverless-apigw-binary": "^0.4.4",
    "serverless-offline": "^11.1.3",
    "typescript": "~4.7.2"
  }
}

serve:ssr and dev:ssr execute correctly, I can visit the application in the browser in its SSR form. build:serverless also executes correctly, and the main.js is generated properly. When I run serve:serverless, I get this error:

Unexpected "handler" function configuration: Expected object received 'lambda.handler'

This is incorrect, as the handler fn does exist and is nested properly in the yml. There is clearly something else wrong, but serverless isn’t letting on to the actual error. Has anyone had any experience with this, or can point me to actual debugging methods to isolate the issue? Neither the forum, documentation or even stack overflow has been of any help, and I’ve been working on this for literally 3 days straight with no forward movement.

I figured out the problem, which I just happened on by sheer luck. The entire issue was the indentation of the serverless.yml. Once I indented this way, it picked it up.

functions:
  api:
    handler: lambda.handler
    events:
      - http: GET {proxy+}
      - http: GET /

Now that this part is working, I still have one more issue I’m running down. The server starts but when I visit the URL I get ‘server.app.use’ is not a function. I can run the app naturally with the serve:ssr script and listening on the 4000 port, so this has to have something to do with the serverless build process that I haven’t found yet. If anyone has any ideas, please let me know.