Skip to content

jlarmstrongiv/create-vercel-http-server-handler

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

70 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

The definitive guide to running servers in Vercel

Versel’s Philosophy

It's possible to deploy an Express.js application as a single Serverless Function, but it comes with drawbacks and should only be used as a migration path. Instead, embrace multiple Serverless Functions as you incrementally migrate to the Vercel platform.

My Philosophy

While there are cases where following Vercel’s approach is beneficial, I disagree with entirely dismissing these frameworks. With frameworks like Express.js and Nest.js, I can:

  • keep my application portable among hosting providers (No vendor locking)
  • take advantage of the enormouse ecosystem of existing packages, tools, and features built around these frameworks, without reinventing the wheel each time (DRY)
  • reference many resources to learn, build, and bug fix my application in a large community (many resources)

The main drawback† is a slightly longer cold start time when initializing the framework and risking the 50MB limit. This package caches your server so this delay will not be an issue in subsequent requests.

Use cases

This package has three main helper functions:

  • createNextHandler (expessjs and nestjs)
  • createVercelHandler (nestjs)
  • createLambdaHandler (nestjs)

Next.js

Install

This quick start assumes you bootstrapped your function with npx create-next-app project-name. However, this package should work with any Zero Config Deployments.

Install this package via npm, npm install create-vercel-http-server-handler

Be sure you have installed the dependencies of your framework in your project, as this package relies on them.

For Express, npm install express

For Nest.js, npm install @nestjs/core @nestjs/common @nestjs/platform-express

Setup

Inside your api folder, create a catch all API route. For example, make a file named [...slug].ts. Inside that file, import this package:

import {
  createNextHandler,
  bootstrapExpress,
  bootstrapNest,
} from 'create-vercel-http-server-handler';

Export default the handler helper for your framework of choice, and disable the bodyParser.

Express.js

export default createNextHandler({
  bootstrap: bootstrapExpress({ app }),
});

export const config = {
  api: {
    bodyParser: false,
  },
};

Nest.js

This package expects you to use the default @nestjs/platform-express under the hood. It will not work with @nestjs/platform-fastify. Check out the example on github.

When using typescript, don’t forget to npm install --save-dev typescript @types/react @types/node

Optionally, create a useGlobal function for Nest.js to apply any global prefixes, pipes, filters, guards, and interceptors. Because Next.js api routes are all prefixed with /api, we recommend you do the same. Only start the server when invoked by the Nest.js CLI. Here is an example src/main.ts:

import { NestFactory } from '@nestjs/core';
import { INestApplication, NestApplicationOptions } from '@nestjs/common';
import { AppModule } from './server/app/app.module';

export const nestApplicationOptions: NestApplicationOptions = {
  logger: false,
};

export async function useGlobal(app: INestApplication) {
  app.setGlobalPrefix('/api');
}

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await useGlobal(app);
  await app.listen(Number(process.env.NEST_PORT) || 3000);
}
if (process.env.CLI === 'NEST') {
  bootstrap();
}

Pass your AppModule and optional useGlobal function to the bootstrapNest helper function inside your [...slug].ts api route.

import {
  createNextHandler,
  bootstrapNest,
} from 'create-vercel-http-server-handler';
import { AppModule } from '../../server/app/app.module';
import { useGlobal, nestApplicationOptions } from '../../main';

export default createNextHandler({
  bootstrap: bootstrapNest({
    AppModule,
    useGlobal,
    nestApplicationOptions,
  }),
  NODE_ENV: process.env.NODE_ENV,
  NEST_PORT: Number(process.env.NEST_PORT),
});

export const config = {
  api: {
    bodyParser: false,
  },
};

Nest.js also relies on experimental TypeScript features. Install the required dependencies:

npm install --save-dev @babel/plugin-transform-runtime babel-plugin-transform-typescript-metadata @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties

Enable the experimental TypeScript features with a .babelrc file:

{
  "presets": ["next/babel"],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "regenerator": true
      }
    ],
    "babel-plugin-transform-typescript-metadata",
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true }]
  ]
}

Edit the tsconfig.json for Next.js:

{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "baseUrl": "./",

    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules", "dist", ".next", ".vercel", "scripts"]
}

Add the tsconfig.nest.json for Nest.js:

{
  "compilerOptions": {
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,

    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true
  },
  "exclude": ["node_modules", "dist", ".next", ".vercel", "scripts"]
}

Don’t forget to install all of Nest.js’ dependencies and dev dependencies. Plus, move relevant scripts, configs, .gitignores, and other files. Consider moving all the server files into src/server/* and moving the main.ts into src/main.ts.

npm install @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata rimraf rxjs

npm install --save-dev @nestjs/cli @nestjs/schematics @nestjs/testing @types/express @types/jest @types/node @types/supertest @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier eslint-plugin-import jest prettier supertest ts-jest ts-loader ts-node tsconfig-paths typescript

Update the nest-cli.json to reflect the new organization:

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src/server",
  "compilerOptions": {
    "plugins": ["@nestjs/graphql/plugin"]
  }
}

Finally, update the scripts inside the package.json and install the required script dependencies:

npm install --save-dev npm-run-all cross-env wait-on

{
  "scripts": {
    "predev": "rimraf dist",
    "dev": "npm-run-all -p -r dev:nest dev:next:wait",
    "dev:next": "cross-env NEST_PORT=7000 next dev -p 8000",
    "dev:next:wait": "npm-run-all -s dev:nest:wait dev:next",
    "dev:nest": "cross-env NEST_PORT=7000 CLI=NEST nest start --path ./tsconfig.nest.json --watch --preserveWatchOutput",
    "dev:nest:wait": "wait-on tcp:7000",
    "build": "npm run build:next",
    "build:next": "next build",
    "prebuild:nest": "rimraf dist",
    "build:nest": "cross-env NODE_ENV=production nest build --path ./tsconfig.nest.json",
    "start": "npm run start:next",
    "start:next": "next start -p 8000",
    "start:nest": "cross-env CLI=NEST NEST_PORT=7000 nest start --path ./tsconfig.nest.json",
    "start:nest:prod": "cross-env CLI=NEST NEST_PORT=7000 node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  }
}

If you are using graphql, customize the webpack config in next.config.js and npm install --save-dev ts-loader:

module.exports = {
  webpack: (config, { isServer }) => {
    if (isServer) {
      const tsLoader = {
        test: /\.tsx?$/,
        loader: 'ts-loader',
        options: {
          transpileOnly: true,
          getCustomTransformers: program => ({
            before: [require('@nestjs/graphql/plugin').before({}, program)],
          }),
        },
        exclude: /node_modules/,
      };
      config.module.rules.push(tsLoader);
    }
    return config;
  },
};

If you are creating custom scripts, you will need another tsconfig.scripts.json:

{
  "compilerOptions": {
    "noEmit": true,

    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,

    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "baseUrl": "./",
    "incremental": true
  },
  "exclude": ["node_modules", "dist", ".next", ".vercel"]
}

Serverless Configuration

For additional configuration, read Vercel’s docs.

vercel.json

{
  "version": 2,
  "scope": "your-scope",
  "functions": {
    "src/pages/api/[...slug].ts": {
      "memory": 3008,
      "maxDuration": 60
    }
  }
}

How it works

The first argument of createNextHandler is your bootstrap function. The second argument is enableCache to cache your server after startup. I recommend using !!process.env.AWS_REGION so that your server is cached on Vercel hosting, but will still hot reload properly locally.

Internally, we call http.createServer(expressApp) and cache your server after calling app.listen(port, () => { … });. We proxy your server by forwarding all of Vercel’s requests via node-http-proxy. Essentially, we are running a server inside a serverless function.

AWS Lambda

Install

This QuickStart assumes you bootstrapped your function with nest new project-name. However, this package should work with any Zero Config Deployments.

Install this package via npm, npm install create-vercel-http-server-handler

Be sure you have installed the dependencies of your framework in your project, as this package relies on them.

For express, npm install aws-serverless-express @nestjs/platform-express

For claudia, npm install claudia --save-dev

TypeScript

Edit your tsconfig.json file:

{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "isolatedModules": true,

    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true
  }
}

Setup

Inside your source folder, extract all your global app settings into src/useGlobal.ts:

import helmet from 'helmet'; // npm i helmet
import { UseGlobal } from 'create-vercel-http-server-handler';

export const useGlobal: UseGlobal = async app => {
  app.use(helmet());
  app.enableCors();

  // only exists in NestExpressApplication
  if ('disable' in app) app.disable('x-powered-by');

  return app;
};

Refactor your src/main.ts to use the useGlobal.ts function:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module';
import { useGlobal } from './useGlobal';

async function start() {
  const app = await NestFactory.create(AppModule);
  await useGlobal(app);
  await app.listen(4000);
}
start();

Create your src/lambda.ts file:

import { createLambdaHandler } from 'create-vercel-http-server-handler';
import { AppModule } from './app/app.module';
import { useGlobal } from './useGlobal';

module.exports.handler = createLambdaHandler({
  AppModule,
  useGlobal,
});

Claudia.js

We will deploy our function with Claudia.js. Be sure to follow their setup instructions.

Unfortunately, Claudia.js does not support native modules (which are c++ libraries built for specific NodeJS versions and operating systems), like sharp.js. We will adapt steps from this guide to script our own support. Be sure you have docker installed.

Scripts

Inside project-name/scripts/claudia-create.sh:

docker run -v $PWD:/claudia -v $HOME/.aws:/root/.aws --rm lambci/lambda:build-nodejs12.x /bin/bash -c "\
cd /claudia
rm -rf node_modules
npm install
npm run build
npm run claudia-create
"

Inside project-name/scripts/claudia-update.sh:

docker run -v $PWD:/claudia -v $HOME/.aws:/root/.aws --rm lambci/lambda:build-nodejs12.x /bin/bash -c "\
cd /claudia
rm -rf node_modules
npm install
npm run build
npm run claudia-update
"

Make the scripts executable by running chmod +x ./scripts/claudia-create.sh and chmod +x ./scripts/claudia-update.sh in your terminal. You should only need to do this once.

Add these scripts to your package.json:

{
  "claudia-create": "claudia create --handler dist/lambda.handler --deploy-proxy-api --region us-east-1 --timeout 29",
  "claudia-update": "claudia update --timeout 29",
  "create": "./scripts/claudia-create.sh",
  "update": "./scripts/claudia-update.sh"
}

To run Nest.js locally again, be sure to reinstall your dependencies with npx rimraf node_modules and npm install. You will need to do this after every deployment.

Finally, you will need to add these fields to your package.json for Claudia.js to behave correctly:

{
  "files": ["dist"],
  "main": "lambda.js"
}

or

{
  "main": "dist/lambda.js"
}

Avoiding the 50MB limit

No matter what, claudiajs will always include your production dependencies in your node_modules folder. However, there are often unnecessary files included with your package.

To avoid hitting the 50MB limit, use a package to prune the node_modules folder using:

Modclean

Run npm install modclean --save-dev

Add a postinstall script to your package.json:

{
  "postinstall": "npx modclean -n default:caution --no-progress --run"
}

Inside your docker scripts, add npm set unsafe-perm true before running any npm commands to fix the cannot run in wd error.

Vercel Serverless

This QuickStart assumes you bootstrapped your function with nest new project-name. However, this package should work with any Zero Config Deployments.

Install this package via npm, npm install create-vercel-http-server-handler and npm install --save-dev vercel

TypeScript

Edit your tsconfig.json file:

{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "isolatedModules": true,

    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true
  }
}

Setup

Inside your source folder, extract all your global app settings into src/useGlobal.ts:

import helmet from 'helmet'; // npm i helmet
import { UseGlobal } from 'create-vercel-http-server-handler';

export const useGlobal: UseGlobal = async app => {
  app.use(helmet());
  app.enableCors();

  // only exists in NestExpressApplication
  if ('disable' in app) app.disable('x-powered-by');

  return app;
};

Refactor your src/main.ts to use the useGlobal.ts function:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module';
import { useGlobal } from './useGlobal';

async function start() {
  const app = await NestFactory.create(AppModule);
  await useGlobal(app);
  await app.listen(4000);
}
start();

Create your src/vercel.ts file:

import { createVercelHandler } from 'create-vercel-http-server-handler';
import { AppModule } from './app/app.module';
import { useGlobal } from './useGlobal';

export default createVercelHandler({
  AppModule,
  useGlobal,
});

Vercel

Edit your vercel.json file:

{
  "version": 2,
  "cleanUrls": true,
  "rewrites": [
    { "source": "/api/vercel", "destination": "/api/vercel" },
    { "source": "/", "destination": "/api/vercel" },
    { "source": "/:match*", "destination": "/api/vercel" }
  ]
}

Note: the root path is a bit buggy. Switch the example @Get() to @Get('/hello') in src/app/app.controller.ts to demo.

Add project-name/api/vercel.js:

import Handler from '../dist/vercel';

export default Handler;

export const config = {
  api: {
    bodyParser: false,
  },
};

Run npx vercel to setup and deploy your project. Choose the defaults, except for the Output Directory option—select dist, where Nestjs compiles your code (otherwise you will have an infinite loop with npx vercel dev).

To deploy to production, run npx vercel --prod. Vercel handles all native dependencies for you automatically.

Footnotes

† Please note that serverless in general does not scale well when directly connecting to a database like MongoDB or PostgreSQL. Be sure you use connection pools.

Thank you

This package would not exist without the help of: