Migrate from Vercel to Serverless
Code examples and snippets.
A lot of people have been migrating to Vercel recently, but I found myself wanting to migrate 15 domains and about 20 projects from it, into somewhere I had a bit more control over.
Why I wanted to leave Vercel isn't really important to debate publicly, but I found their platform too opaque in terms of usage and I recently had a few bad experiences with their customer support.
So, naturally I wanted to use something that was Open Source, and which used either DigitalOcean or AWS. I found Serverless and after some research (I'd learned about it before) it seemed like the best candidate, deploying to AWS.
One important thing to note is that my projects were all in either "plain" Node.js, Node.js with TypeScript, plain "static" (HTML, JS, CSS, images/files), or Next.js 9+. This probably made it incredibly easier to migrate than, for example, using other technologies.
You can see a lot of these migrations in public repos, like my simple Next.js boilerplate, the one with a markdown blog, and FaunaDB + ElasticSearch. Coupled with the BudgetZen web app, you can see a few variations and the important things to note for migrating a Next.js app.
I can also show some other snippets from private repos. Here's a simple one where I have some conditional redirects.
An old vercel.json
file that used to have:
{
"version": 2,
"scope": "brn",
"alias": ["thoughts.brunobernardino.com"],
"routes": [
{
"src": "/no-ambition-no-goals/?",
"status": 301,
"headers": {
"Location": "https://brunobernardino.com/mindfulness"
}
},
{
"src": "/beefing-up-privacy/?",
"status": 301,
"headers": {
"Location": "https://brunobernardino.com/privacy"
}
},
{
"src": "/using-limits-as-expanders/?",
"status": 301,
"headers": {
"Location": "https://brunobernardino.com/simplify"
}
},
{
"src": "/.*",
"status": 301,
"headers": {
"Location": "https://brunobernardino.com"
}
}
]
}
Now it requires 3 files: serverless.yml
, handler.js
, and package.json
. They look like this:
serverless.yml
service: thoughts-redirect
provider: name: aws runtime: nodejs12.x region: us-east-1 profile: BRN
plugins: - serverless-domain-manager
custom: customDomain: certificateName: "brunobernardino.com" domainName: thoughts.brunobernardino.com basePath: '' createRoute53Record: true
functions: index: handler: handler.redirect events: - http: path: /{any+} method: get integration: lambda-proxy - http: path: / method: get integration: lambda-proxy
handler.js
'use strict';
module.exports.redirect = (event, context, callback) => { const requestPath = event.path;
let finalDomain = 'https://brunobernardino.com';
if (requestPath.startsWith('/using-limits-as-expanders')) { finalDomain = 'https://brunobernardino.com/simplify'; }
if (requestPath.startsWith('/beefing-up-privacy')) { finalDomain = 'https://brunobernardino.com/privacy'; }
if (requestPath.startsWith('/no-ambition-no-goals')) { finalDomain = 'https://brunobernardino.com/mindfulness'; }
const response = { statusCode: 301, headers: { Location: finalDomain, }, body: '', };
callback(null, response); };
package.json
{
"name": "thoughts",
"version": "1.0.0",
"private": true,
"description": "",
"main": "index.js",
"dependencies": {},
"devDependencies": {
"serverless-domain-manager": "5.1.0"
},
"scripts": {},
"keywords": [],
"author": "Bruno Bernardino <me@brunobernardino.com>",
"license": "UNLICENSED"
}
That might seem like a lot more boilerplate, but it also means a lot more control over what's happening. To deploy I just run serverless deploy
, after a first-time-only serverless create_domain
(this will require the domain to be in Route53, but I'll go deeper in detail about that below).
A simple page loader that returns the same text/HTML (not an HTML page), can be converted from a simple index.html
and a vercel.json
like this:
index.html
👋
vercel.json
{
"version": 2,
"scope": "brn",
"alias": ["domain1.com", "domain2.com", "domain3.com"]
}
To 3 files: serverless.yml
, handler.js
, and package.json
(which looks the same as above). The first two now look like this:
serverless.yml
service: example-website
provider: name: aws runtime: nodejs12.x region: us-east-1 profile: BRN
plugins:
- serverless-domain-manager
custom: customDomains: - rest: certificateName: "domain1.com" domainName: domain1.com createRoute53Record: true - rest: certificateName: "domain2.com" domainName: domain2.com createRoute53Record: true - rest: certificateName: "domain3.com" domainName: domain3.com createRoute53Record: true
functions: index: handler: handler.serve events: - http: path: / method: get integration: lambda-proxy
handler.js
'use strict';
module.exports.serve = (event, context, callback) => { const response = { statusCode: 200, body: '👋', };
callback(null, response); };
And what about a simple static website?
Great question! I've got a few of those, and you can see a public migration which shows how simple that can be at my web3-type-converter. Again, having the domain hosted on Route53 is a requirement, but you were likely hosting the domain on Vercel before anyway.
And a basic backend-only app? I mean, it's serverless!
Right, that's the most common scenario, so I didn't think it would be complicated, and it wasn't! I've got a service that gets the top 5 HackerNews stories and adds them to a feed reader. This runs via a GitHub Action (cron, daily), and is perfect for a serverless setup. I won't get into all the details, but the main change I had to make was converting the handler. It looked something like this:
old last bit of handler.js
export default async (req, res) => { const { token } = req.query;
if (!token || typeof token !== 'string' || token !== process.env.SECRET_TOKEN) { res.status(401).json({ code: 1, message: 'Token is Required' }); return; }
await main();
res.status(200).json({ code: 0, message: 'Stuff done' }); };
old vercel.json
{
"version": 2,
"scope": "brn",
"alias": ["hackernews-job.example.com"],
"github": {
"enabled": false
},
"env": {
"BLOGINMAIL_API_KEY": "@secret-bloginmail-api-key",
"SECRET_TOKEN": "@secret-hackernews-token"
},
"builds": [
{
"src": "index.js",
"use": "@vercel/node"
}
]
}
And so I just had to adapt the request and response variables, accordingly. It became something like this:
new last bit of handler.js
module.exports.handle = async (event, context, callback) => { const { token } = event.queryStringParameters;
const response = { statusCode: 200, headers: { 'Content-Type': 'application/json', }, body: '{}', };
if (!token || typeof token !== 'string' || token !== process.env.SECRET_TOKEN) { response.statusCode = 401; response.body = JSON.stringify({ code: 1, message: 'Token is Required' }); return response; }
await main();
response.body = JSON.stringify({ code: 0, message: 'Stuff done' });
return response; };
serverless.yml
service: hackernews-top-stories
provider: name: aws runtime: nodejs12.x region: us-east-1 profile: BRN timeout: 30 lambdaHashingVersion: 20201221 apiGateway: shouldStartNameWithService: true
plugins:
- serverless-dotenv-plugin
- serverless-offline
- serverless-domain-manager
package: excludeDevDependencies: true
custom: customDomain: certificateName: "example.com" domainName: hackernews-job.example.com basePath: '' createRoute53Record: true serverless-offline: noPrependStageInUrl: true
functions: index: handler: handler.handle events: - http: path: / method: get
To start it, I changed from vercel dev
to serverless offline
, and to deploy I use serverless deploy
.
Done!
And that's it! I think I covered the different types of applications I had in Vercel that I migrated
Finishing notes: DNS and cost/price
A couple of important things to note is that migrating DNS from Vercel is actually a PITA. They allow importing Zone files, but do not export them (which I personally found shady), so I had to manually add them in Route53. It took some time.
I didn't do this because of cost, but my cost went down from $20 / month to less than $10 / month, mostly because of the 15 domains hosted in Route53 ($7.5 / month). I have a few hundred thousands of requests per month, so I qualify for the free tier in many things (not S3 nor CloudFront, though).
Thank you so much for being here. I really appreciate you!