[
  {
    "path": ".gitignore",
    "content": "package-lock.json\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Typescript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n.npmrc\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# dotenv environment variables file\n.env\n.env.development\n\n# gatsby files\n.cache/\npublic\n\n# Mac files\n.DS_Store\n\n# Yarn\nyarn-error.log\n.pnp/\n.pnp.js\n# Yarn Integrity file\n.yarn-integrity\n\n.now\n.serverless\n"
  },
  {
    "path": ".prettierignore",
    "content": ".cache\npackage.json\npackage-lock.json\npublic\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"endOfLine\": \"lf\",\n  \"semi\": false,\n  \"singleQuote\": false,\n  \"tabWidth\": 2,\n  \"trailingComma\": \"es5\"\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"python.pythonPath\": \"/usr/local/bin/python2.7\"\n}"
  },
  {
    "path": ".yarnrc",
    "content": "\"@swizec:registry\" \"https://registry.npmjs.org/\"\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 gatsbyjs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "\n# Serverless Handbook for frontend engineers\n\nCurious about serverless? Wanna get into backend but not sure how? This handbook is for you. \n\nRather than a book you read start to finish, it's meant to work as a reference. A handbook that stands by your side while you work. Need a recipe? Look it up. Got a question on a thorny issue? The handbook hopes to help.\n\nOpen source, contributions and fixes welcome :)"
  },
  {
    "path": "examples/hello-world/handler.js",
    "content": "exports.hello = async (event) => {\n  const { name = '' } = event.queryStringParameters || {}\n\n  return {\n    statusCode: 200,\n    body: `Hello ${name} 👋`,\n  }\n}\n"
  },
  {
    "path": "examples/hello-world/serverless.yml",
    "content": "service: hello-world\nprovider:\n    name: aws\n    runtime: nodejs12.x\n    stage: dev\n\nfunctions:\n    hello:\n        handler: ./handler.hello\n        events:\n            - http:\n                  path: hello\n                  method: GET\n                  cors: true\n"
  },
  {
    "path": "examples/serverless-auth-example/.gitignore",
    "content": "dist\nnode_modules\n.serverless\n"
  },
  {
    "path": "examples/serverless-auth-example/package.json",
    "content": "{\n  \"name\": \"serverless-auth-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"AWS Lambda example of building a simple auth\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"deploy\": \"npm run build && serverless deploy\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/Swizec/serverless-handbook.git\"\n  },\n  \"author\": \"Swizec\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/Swizec/serverless-handbook/issues\"\n  },\n  \"homepage\": \"https://serverlesshandbook.dev\",\n  \"dependencies\": {\n    \"@types/aws-lambda\": \"^8.10.72\",\n    \"@types/crypto-js\": \"^4.0.1\",\n    \"@types/jsonwebtoken\": \"^8.5.0\",\n    \"@types/lodash.omit\": \"^4.5.6\",\n    \"aws-lambda\": \"^1.0.6\",\n    \"crypto-js\": \"^4.0.0\",\n    \"jsonwebtoken\": \"^8.5.1\",\n    \"lodash.omit\": \"^4.5.0\",\n    \"simple-dynamodb\": \"^1.0.1\",\n    \"typescript\": \"^4.1.5\",\n    \"uuid\": \"^8.3.2\"\n  },\n  \"engines\": {\n    \"node\": \"12.x || 14.x\"\n  }\n}\n"
  },
  {
    "path": "examples/serverless-auth-example/serverless.yml",
    "content": "service: serverless-auth-example\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: dev\n  environment:\n    USER_TABLE: ${self:service}-users-${self:provider.stage}\n    SALT: someRandomSecretString_pleaseUseProperSecrets:)\n    JWT_SECRET: useRealSecretsManagementPlease\n  iamRoleStatements:\n    - Effect: Allow\n      Action:\n        - dynamodb:Query\n        - dynamodb:Scan\n        - dynamodb:GetItem\n        - dynamodb:PutItem\n        - dynamodb:UpdateItem\n        - dynamodb:DeleteItem\n      Resource: \"arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.USER_TABLE}\"\n\nfunctions:\n  login:\n    handler: dist/auth.login\n    events:\n      - http:\n          path: login\n          method: POST\n          cors: true\n  verify:\n    handler: dist/auth.verify\n    events:\n      - http:\n          path: verify\n          method: POST\n          cors: true\n  privateHello:\n    handler: dist/private.hello\n    events:\n      - http:\n          path: private\n          method: GET\n          cors: true\n\nresources:\n  Resources:\n    UsersTable:\n      Type: \"AWS::DynamoDB::Table\"\n      Properties:\n        AttributeDefinitions:\n          - AttributeName: username\n            AttributeType: S\n        KeySchema:\n          - AttributeName: username\n            KeyType: HASH\n        ProvisionedThroughput:\n          ReadCapacityUnits: 1\n          WriteCapacityUnits: 1\n        TableName: ${self:provider.environment.USER_TABLE}\n\npackage:\n  exclude:\n    - node_modules/typescript/**\n    - node_modules/@types/**\n"
  },
  {
    "path": "examples/serverless-auth-example/src/auth.ts",
    "content": "import { APIGatewayEvent } from \"aws-lambda\"\nimport * as db from \"simple-dynamodb\"\nimport omit from \"lodash.omit\"\nimport * as jwt from \"jsonwebtoken\"\nimport { response, hashPassword } from \"./util\"\n\nasync function createUser(username: string, password: string) {\n  const result = await db.updateItem({\n    TableName: process.env.USER_TABLE!,\n    Key: {\n      username,\n    },\n    UpdateExpression: `SET password = :password, createdAt = :createdAt`,\n    ExpressionAttributeValues: {\n      \":password\": hashPassword(username, password),\n      \":createdAt\": new Date().toISOString(),\n    },\n  })\n  return result.Attributes\n}\n\nasync function findUser(username: string) {\n  const result = await db.getItem({\n    TableName: process.env.USER_TABLE!,\n    Key: {\n      // username is the key, which means it must be unique\n      username,\n    },\n  })\n\n  return result.Item\n}\n\n// Logs you in based on username/password combo\n// Creates user on first login\nexport const login = async (event: APIGatewayEvent) => {\n  const { username, password } = JSON.parse(event.body || \"{}\")\n\n  if (!username || !password) {\n    return response(400, {\n      status: \"error\",\n      error: \"Please provide a username and password\",\n    })\n  }\n\n  // find user in database\n  let user = await findUser(username)\n\n  if (!user) {\n    // user was not found, create\n    user = await createUser(username, password)\n  } else {\n    // check credentials\n    if (hashPassword(username, password) !== user.password) {\n      // 🚨\n      return response(401, {\n        status: \"error\",\n        error: \"Bad username/password combination\",\n      })\n    }\n  }\n\n  // user was created or has valid credentials\n  const token = jwt.sign(omit(user, \"password\"), process.env.SUPER_SECRET!)\n\n  return response(200, {\n    user: omit(user, \"password\"),\n    token,\n  })\n}\n\n// Verifies you have a valid JWT token\nexport const verify = async (event: APIGatewayEvent) => {\n  const { token } = JSON.parse(event.body || \"{}\")\n\n  if (!token) {\n    return response(400, {\n      status: \"error\",\n      error: \"Please provide a token to verify\",\n    })\n  }\n\n  try {\n    jwt.verify(token, process.env.JWT_SECRET!)\n    return response(200, { status: \"valid\" })\n  } catch (err) {\n    return response(401, err)\n  }\n}\n"
  },
  {
    "path": "examples/serverless-auth-example/src/private.ts",
    "content": "import { APIGatewayEvent } from \"aws-lambda\"\nimport { response, checkAuth, User } from \"./util\"\n\nexport async function hello(event: APIGatewayEvent) {\n  // returns JWT token payload\n  const user = checkAuth(event) as User\n\n  if (user) {\n    return response(200, {\n      message: `Hello ${user.username}`,\n    })\n  } else {\n    return response(401, {\n      status: \"error\",\n      error: \"This is a private resource\",\n    })\n  }\n}\n"
  },
  {
    "path": "examples/serverless-auth-example/src/util.ts",
    "content": "import { APIGatewayEvent } from \"aws-lambda\"\nimport sha256 from \"crypto-js/sha256\"\nimport * as jwt from \"jsonwebtoken\"\n\nexport function response(statusCode: number, body: any) {\n  return {\n    statusCode,\n    // permissive CORS headers\n    headers: {\n      \"Access-Control-Allow-Headers\": \"Content-Type\",\n      \"Access-Control-Allow-Origin\": \"*\",\n      \"Access-Control-Allow-Methods\": \"OPTIONS,POST,GET\",\n    },\n    body: JSON.stringify(body),\n  }\n}\n\n// Hashing your password before saving is critical\n// Hashing is one-way meaning you can never guess the password\n// Adding a salt and the username guards against common passwords\nexport function hashPassword(username: string, password: string) {\n  return sha256(\n    `${password}${process.env.SALT}${username}${password}`\n  ).toString()\n}\n\nexport type User = { username: string; createdAt: string }\n\n// Used to verify a request is authenticated\nexport function checkAuth(event: APIGatewayEvent): boolean | User {\n  const bearer = event.headers[\"Authorization\"]\n\n  if (bearer) {\n    try {\n      const decoded = jwt.verify(\n        // Bearer prefix from Authorization header\n        bearer.replace(/^Bearer /, \"\"),\n        process.env.JWT_SECRET!\n      )\n\n      // We saved user info in the token\n      return decoded as User\n    } catch (err) {\n      return false\n    }\n  } else {\n    return false\n  }\n}\n"
  },
  {
    "path": "examples/serverless-auth-example/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2019\",\n    \"module\": \"commonjs\",\n    \"outDir\": \"./dist\",\n    \"strict\": true,\n    \"baseUrl\": \"./\",\n    \"typeRoots\": [\"node_modules/@types\"],\n    \"types\": [\"node\"],\n    \"esModuleInterop\": true,\n    \"inlineSourceMap\": true,\n    \"lib\": [\"ES2019\", \"dom\"]\n  }\n}\n"
  },
  {
    "path": "examples/serverless-chrome-example/dist/scraper.js",
    "content": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst util_1 = require(\"./util\");\nasync function scrapeGoogle(browser, search) {\n    const page = await browser.newPage();\n    await page.goto(\"https://google.com\", {\n        waitUntil: [\"domcontentloaded\", \"networkidle2\"],\n    });\n    // this part is specific to the page you're scraping\n    await page.type(\"input[type=text]\", search);\n    const [response] = await Promise.all([\n        page.waitForNavigation(),\n        page.click(\"input[type=submit]\"),\n    ]);\n    if (!response.ok()) {\n        throw \"Couldn't get response\";\n    }\n    await page.goto(response.url());\n    // this part is very specific to the page you're scraping\n    const searchResults = await page.$$(\".rc\");\n    let links = await Promise.all(searchResults.map(async (result) => {\n        return {\n            url: await result.$eval(\"a\", (node) => node.getAttribute(\"href\")),\n            title: await result.$eval(\"h3\", (node) => node.innerHTML),\n            description: await result.$eval(\"span.st\", (node) => node.innerHTML),\n        };\n    }));\n    return {\n        statusCode: 200,\n        body: JSON.stringify(links),\n    };\n}\nexports.handler = util_1.createHandler(scrapeGoogle);\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2NyYXBlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9zY3JhcGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBRUEsaUNBQXNDO0FBRXRDLEtBQUssVUFBVSxZQUFZLENBQUMsT0FBZ0IsRUFBRSxNQUFjO0lBQzFELE1BQU0sSUFBSSxHQUFHLE1BQU0sT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFBO0lBQ3BDLE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxvQkFBb0IsRUFBRTtRQUNwQyxTQUFTLEVBQUUsQ0FBQyxrQkFBa0IsRUFBRSxjQUFjLENBQUM7S0FDaEQsQ0FBQyxDQUFBO0lBRUYsb0RBQW9EO0lBQ3BELE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxrQkFBa0IsRUFBRSxNQUFNLENBQUMsQ0FBQTtJQUUzQyxNQUFNLENBQUMsUUFBUSxDQUFDLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDO1FBQ25DLElBQUksQ0FBQyxpQkFBaUIsRUFBRTtRQUN4QixJQUFJLENBQUMsS0FBSyxDQUFDLG9CQUFvQixDQUFDO0tBQ2pDLENBQUMsQ0FBQTtJQUVGLElBQUksQ0FBQyxRQUFRLENBQUMsRUFBRSxFQUFFLEVBQUU7UUFDbEIsTUFBTSx1QkFBdUIsQ0FBQTtLQUM5QjtJQUVELE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxFQUFFLENBQUMsQ0FBQTtJQUUvQix5REFBeUQ7SUFDekQsTUFBTSxhQUFhLEdBQUcsTUFBTSxJQUFJLENBQUMsRUFBRSxDQUFDLEtBQUssQ0FBQyxDQUFBO0lBRTFDLElBQUksS0FBSyxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FDM0IsYUFBYSxDQUFDLEdBQUcsQ0FBQyxLQUFLLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDakMsT0FBTztZQUNMLEdBQUcsRUFBRSxNQUFNLE1BQU0sQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsWUFBWSxDQUFDLE1BQU0sQ0FBQyxDQUFDO1lBQ2pFLEtBQUssRUFBRSxNQUFNLE1BQU0sQ0FBQyxLQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDO1lBQ3pELFdBQVcsRUFBRSxNQUFNLE1BQU0sQ0FBQyxLQUFLLENBQUMsU0FBUyxFQUFFLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDO1NBQ3JFLENBQUE7SUFDSCxDQUFDLENBQUMsQ0FDSCxDQUFBO0lBRUQsT0FBTztRQUNMLFVBQVUsRUFBRSxHQUFHO1FBQ2YsSUFBSSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDO0tBQzVCLENBQUE7QUFDSCxDQUFDO0FBRVksUUFBQSxPQUFPLEdBQUcsb0JBQWEsQ0FBQyxZQUFZLENBQUMsQ0FBQSJ9"
  },
  {
    "path": "examples/serverless-chrome-example/dist/screenshot.js",
    "content": "\"use strict\";\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n    return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst fs_1 = __importDefault(require(\"fs\"));\nconst util_1 = require(\"./util\");\nasync function screenshotGoogle(browser, search) {\n    const page = await browser.newPage();\n    await page.goto(\"https://google.com\", {\n        waitUntil: [\"domcontentloaded\", \"networkidle2\"],\n    });\n    // this part is specific to the page you're screenshotting\n    await page.type(\"input[type=text]\", search);\n    const [response] = await Promise.all([\n        page.waitForNavigation(),\n        page.click(\"input[type=submit]\"),\n    ]);\n    if (!response.ok()) {\n        throw \"Couldn't get response\";\n    }\n    await page.goto(response.url());\n    // this part is specific to the page you're screenshotting\n    const element = await page.$(\"#main\");\n    if (!element) {\n        throw \"Couldn't find results div\";\n    }\n    const boundingBox = await element.boundingBox();\n    const imagePath = `/tmp/screenshot-${new Date().getTime()}.png`;\n    if (!boundingBox) {\n        throw \"Couldn't measure size of results div\";\n    }\n    await page.screenshot({\n        path: imagePath,\n        clip: boundingBox,\n    });\n    const data = fs_1.default.readFileSync(imagePath).toString(\"base64\");\n    return {\n        statusCode: 200,\n        headers: {\n            \"Content-Type\": \"image/png\",\n        },\n        body: data,\n        isBase64Encoded: true,\n    };\n}\nexports.handler = util_1.createHandler(screenshotGoogle);\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2NyZWVuc2hvdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9zY3JlZW5zaG90LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBQ0EsNENBQW1CO0FBRW5CLGlDQUFzQztBQUV0QyxLQUFLLFVBQVUsZ0JBQWdCLENBQUMsT0FBZ0IsRUFBRSxNQUFjO0lBQzlELE1BQU0sSUFBSSxHQUFHLE1BQU0sT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFBO0lBQ3BDLE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxvQkFBb0IsRUFBRTtRQUNwQyxTQUFTLEVBQUUsQ0FBQyxrQkFBa0IsRUFBRSxjQUFjLENBQUM7S0FDaEQsQ0FBQyxDQUFBO0lBRUYsMERBQTBEO0lBQzFELE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxrQkFBa0IsRUFBRSxNQUFNLENBQUMsQ0FBQTtJQUUzQyxNQUFNLENBQUMsUUFBUSxDQUFDLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDO1FBQ25DLElBQUksQ0FBQyxpQkFBaUIsRUFBRTtRQUN4QixJQUFJLENBQUMsS0FBSyxDQUFDLG9CQUFvQixDQUFDO0tBQ2pDLENBQUMsQ0FBQTtJQUVGLElBQUksQ0FBQyxRQUFRLENBQUMsRUFBRSxFQUFFLEVBQUU7UUFDbEIsTUFBTSx1QkFBdUIsQ0FBQTtLQUM5QjtJQUVELE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxFQUFFLENBQUMsQ0FBQTtJQUUvQiwwREFBMEQ7SUFDMUQsTUFBTSxPQUFPLEdBQUcsTUFBTSxJQUFJLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFBO0lBRXJDLElBQUksQ0FBQyxPQUFPLEVBQUU7UUFDWixNQUFNLDJCQUEyQixDQUFBO0tBQ2xDO0lBRUQsTUFBTSxXQUFXLEdBQUcsTUFBTSxPQUFPLENBQUMsV0FBVyxFQUFFLENBQUE7SUFDL0MsTUFBTSxTQUFTLEdBQUcsbUJBQW1CLElBQUksSUFBSSxFQUFFLENBQUMsT0FBTyxFQUFFLE1BQU0sQ0FBQTtJQUUvRCxJQUFJLENBQUMsV0FBVyxFQUFFO1FBQ2hCLE1BQU0sc0NBQXNDLENBQUE7S0FDN0M7SUFFRCxNQUFNLElBQUksQ0FBQyxVQUFVLENBQUM7UUFDcEIsSUFBSSxFQUFFLFNBQVM7UUFDZixJQUFJLEVBQUUsV0FBVztLQUNsQixDQUFDLENBQUE7SUFFRixNQUFNLElBQUksR0FBRyxZQUFFLENBQUMsWUFBWSxDQUFDLFNBQVMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsQ0FBQTtJQUUxRCxPQUFPO1FBQ0wsVUFBVSxFQUFFLEdBQUc7UUFDZixPQUFPLEVBQUU7WUFDUCxjQUFjLEVBQUUsV0FBVztTQUM1QjtRQUNELElBQUksRUFBRSxJQUFJO1FBQ1YsZUFBZSxFQUFFLElBQUk7S0FDdEIsQ0FBQTtBQUNILENBQUM7QUFFWSxRQUFBLE9BQU8sR0FBRyxvQkFBYSxDQUFDLGdCQUFnQixDQUFDLENBQUEifQ=="
  },
  {
    "path": "examples/serverless-chrome-example/dist/util.js",
    "content": "\"use strict\";\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n    return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst chrome_aws_lambda_1 = __importDefault(require(\"chrome-aws-lambda\"));\nasync function getChrome() {\n    let browser = null;\n    try {\n        browser = await chrome_aws_lambda_1.default.puppeteer.launch({\n            args: chrome_aws_lambda_1.default.args,\n            defaultViewport: {\n                width: 1920,\n                height: 1080,\n                isMobile: true,\n                deviceScaleFactor: 2,\n            },\n            executablePath: await chrome_aws_lambda_1.default.executablePath,\n            headless: chrome_aws_lambda_1.default.headless,\n            ignoreHTTPSErrors: true,\n        });\n    }\n    catch (err) {\n        console.error(\"Error launching chrome\");\n        console.error(err);\n    }\n    return browser;\n}\nexports.getChrome = getChrome;\n// both scraper and screenshot have the same basic handler\n// they just call a different method to do things\nexports.createHandler = (workFunction) => async (event) => {\n    const search = event.queryStringParameters && event.queryStringParameters.search;\n    if (!search) {\n        return {\n            statusCode: 400,\n            body: \"Please provide a ?search= parameter\",\n        };\n    }\n    const browser = await getChrome();\n    if (!browser) {\n        return {\n            statusCode: 500,\n            body: \"Error launching Chrome\",\n        };\n    }\n    try {\n        // call the function that does the real work\n        const response = await workFunction(browser, search);\n        return response;\n    }\n    catch (err) {\n        console.log(err);\n        return {\n            statusCode: 500,\n            body: \"Error scraping Google\",\n        };\n    }\n};\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy91dGlsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBRUEsMEVBQXNDO0FBUy9CLEtBQUssVUFBVSxTQUFTO0lBQzdCLElBQUksT0FBTyxHQUFHLElBQUksQ0FBQTtJQUVsQixJQUFJO1FBQ0YsT0FBTyxHQUFHLE1BQU0sMkJBQU0sQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDO1lBQ3RDLElBQUksRUFBRSwyQkFBTSxDQUFDLElBQUk7WUFDakIsZUFBZSxFQUFFO2dCQUNmLEtBQUssRUFBRSxJQUFJO2dCQUNYLE1BQU0sRUFBRSxJQUFJO2dCQUNaLFFBQVEsRUFBRSxJQUFJO2dCQUNkLGlCQUFpQixFQUFFLENBQUM7YUFDckI7WUFDRCxjQUFjLEVBQUUsTUFBTSwyQkFBTSxDQUFDLGNBQWM7WUFDM0MsUUFBUSxFQUFFLDJCQUFNLENBQUMsUUFBUTtZQUN6QixpQkFBaUIsRUFBRSxJQUFJO1NBQ3hCLENBQUMsQ0FBQTtLQUNIO0lBQUMsT0FBTyxHQUFHLEVBQUU7UUFDWixPQUFPLENBQUMsS0FBSyxDQUFDLHdCQUF3QixDQUFDLENBQUE7UUFDdkMsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtLQUNuQjtJQUVELE9BQU8sT0FBTyxDQUFBO0FBQ2hCLENBQUM7QUF0QkQsOEJBc0JDO0FBRUQsMERBQTBEO0FBQzFELGlEQUFpRDtBQUNwQyxRQUFBLGFBQWEsR0FBRyxDQUMzQixZQUF3RSxFQUN4RSxFQUFFLENBQUMsS0FBSyxFQUFFLEtBQXNCLEVBQXdCLEVBQUU7SUFDMUQsTUFBTSxNQUFNLEdBQ1YsS0FBSyxDQUFDLHFCQUFxQixJQUFJLEtBQUssQ0FBQyxxQkFBcUIsQ0FBQyxNQUFNLENBQUE7SUFFbkUsSUFBSSxDQUFDLE1BQU0sRUFBRTtRQUNYLE9BQU87WUFDTCxVQUFVLEVBQUUsR0FBRztZQUNmLElBQUksRUFBRSxxQ0FBcUM7U0FDNUMsQ0FBQTtLQUNGO0lBRUQsTUFBTSxPQUFPLEdBQUcsTUFBTSxTQUFTLEVBQUUsQ0FBQTtJQUVqQyxJQUFJLENBQUMsT0FBTyxFQUFFO1FBQ1osT0FBTztZQUNMLFVBQVUsRUFBRSxHQUFHO1lBQ2YsSUFBSSxFQUFFLHdCQUF3QjtTQUMvQixDQUFBO0tBQ0Y7SUFFRCxJQUFJO1FBQ0YsNENBQTRDO1FBQzVDLE1BQU0sUUFBUSxHQUFHLE1BQU0sWUFBWSxDQUFDLE9BQU8sRUFBRSxNQUFNLENBQUMsQ0FBQTtRQUVwRCxPQUFPLFFBQVEsQ0FBQTtLQUNoQjtJQUFDLE9BQU8sR0FBRyxFQUFFO1FBQ1osT0FBTyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQTtRQUNoQixPQUFPO1lBQ0wsVUFBVSxFQUFFLEdBQUc7WUFDZixJQUFJLEVBQUUsdUJBQXVCO1NBQzlCLENBQUE7S0FDRjtBQUNILENBQUMsQ0FBQSJ9"
  },
  {
    "path": "examples/serverless-chrome-example/package.json",
    "content": "{\n  \"name\": \"serverless-chrome-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"AWS Lambda example of using Chrome Puppeteer\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"deploy\": \"npm run build && serverless deploy\"\n  },\n  \"engines\": {\n    \"node\": \"12.x\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/Swizec/serverless-handbook.git\"\n  },\n  \"author\": \"Swizec\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/Swizec/serverless-handbook/issues\"\n  },\n  \"homepage\": \"https://github.com/Swizec/serverless-handbook#readme\",\n  \"dependencies\": {\n    \"@types/aws-lambda\": \"^8.10.58\",\n    \"@types/puppeteer\": \"^3.0.1\",\n    \"aws-lambda\": \"^1.0.6\",\n    \"chrome-aws-lambda\": \"^3.1.1\",\n    \"puppeteer\": \"3.1.0\",\n    \"puppeteer-core\": \"3.1.0\"\n  }\n}\n"
  },
  {
    "path": "examples/serverless-chrome-example/serverless.yml",
    "content": "service: serverless-chrome-example\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: dev\n  apiGateway:\n    binaryMediaTypes:\n      - \"*/*\"\n\nfunctions:\n  scraper:\n    handler: dist/scraper.handler\n    memorysize: 2536\n    timeout: 30\n    events:\n      - http:\n          path: scraper\n          method: GET\n          cors: true\n\n  screenshot:\n    handler: dist/screenshot.handler\n    memorysize: 2536\n    timeout: 30\n    events:\n      - http:\n          path: screenshot\n          method: GET\n          cors: true\n\npackage:\n  exclude:\n    - node_modules/puppeteer/.local-chromium/**\n"
  },
  {
    "path": "examples/serverless-chrome-example/src/scraper.ts",
    "content": "import { Browser } from \"puppeteer\"\n\nimport { createHandler } from \"./util\"\n\nasync function scrapeGoogle(browser: Browser, search: string) {\n  const page = await browser.newPage()\n  await page.goto(\"https://google.com\", {\n    waitUntil: [\"domcontentloaded\", \"networkidle2\"],\n  })\n\n  // this part is specific to the page you're scraping\n  await page.type(\"input[type=text]\", search)\n\n  const [response] = await Promise.all([\n    page.waitForNavigation(),\n    page.click(\"input[type=submit]\"),\n  ])\n\n  if (!response.ok()) {\n    throw \"Couldn't get response\"\n  }\n\n  await page.goto(response.url())\n\n  // this part is very specific to the page you're scraping\n  const searchResults = await page.$$(\".rc\")\n\n  let links = await Promise.all(\n    searchResults.map(async (result) => {\n      return {\n        url: await result.$eval(\"a\", (node) => node.getAttribute(\"href\")),\n        title: await result.$eval(\"h3\", (node) => node.innerHTML),\n        description: await result.$eval(\"span.st\", (node) => node.innerHTML),\n      }\n    })\n  )\n\n  return {\n    statusCode: 200,\n    body: JSON.stringify(links),\n  }\n}\n\nexport const handler = createHandler(scrapeGoogle)\n"
  },
  {
    "path": "examples/serverless-chrome-example/src/screenshot.ts",
    "content": "import { Browser } from \"puppeteer\"\nimport fs from \"fs\"\n\nimport { createHandler } from \"./util\"\n\nasync function screenshotGoogle(browser: Browser, search: string) {\n  const page = await browser.newPage()\n  await page.goto(\"https://google.com\", {\n    waitUntil: [\"domcontentloaded\", \"networkidle2\"],\n  })\n\n  // this part is specific to the page you're screenshotting\n  await page.type(\"input[type=text]\", search)\n\n  const [response] = await Promise.all([\n    page.waitForNavigation(),\n    page.click(\"input[type=submit]\"),\n  ])\n\n  if (!response.ok()) {\n    throw \"Couldn't get response\"\n  }\n\n  await page.goto(response.url())\n\n  // this part is specific to the page you're screenshotting\n  const element = await page.$(\"#main\")\n\n  if (!element) {\n    throw \"Couldn't find results div\"\n  }\n\n  const boundingBox = await element.boundingBox()\n  const imagePath = `/tmp/screenshot-${new Date().getTime()}.png`\n\n  if (!boundingBox) {\n    throw \"Couldn't measure size of results div\"\n  }\n\n  await page.screenshot({\n    path: imagePath,\n    clip: boundingBox,\n  })\n\n  const data = fs.readFileSync(imagePath).toString(\"base64\")\n\n  return {\n    statusCode: 200,\n    headers: {\n      \"Content-Type\": \"image/png\",\n    },\n    body: data,\n    isBase64Encoded: true,\n  }\n}\n\nexport const handler = createHandler(screenshotGoogle)\n"
  },
  {
    "path": "examples/serverless-chrome-example/src/util.ts",
    "content": "import { APIGatewayEvent } from \"aws-lambda\"\nimport { Browser } from \"puppeteer\"\nimport chrome from \"chrome-aws-lambda\"\n\nexport type APIResponse = {\n  statusCode: number\n  headers?: { [key: string]: string }\n  body: string | Buffer\n  isBase64Encoded?: boolean\n}\n\nexport async function getChrome() {\n  let browser = null\n\n  try {\n    browser = await chrome.puppeteer.launch({\n      args: chrome.args,\n      defaultViewport: {\n        width: 1920,\n        height: 1080,\n        isMobile: true,\n        deviceScaleFactor: 2,\n      },\n      executablePath: await chrome.executablePath,\n      headless: chrome.headless,\n      ignoreHTTPSErrors: true,\n    })\n  } catch (err) {\n    console.error(\"Error launching chrome\")\n    console.error(err)\n  }\n\n  return browser\n}\n\n// both scraper and screenshot have the same basic handler\n// they just call a different method to do things\nexport const createHandler = (\n  workFunction: (browser: Browser, search: string) => Promise<APIResponse>\n) => async (event: APIGatewayEvent): Promise<APIResponse> => {\n  const search =\n    event.queryStringParameters && event.queryStringParameters.search\n\n  if (!search) {\n    return {\n      statusCode: 400,\n      body: \"Please provide a ?search= parameter\",\n    }\n  }\n\n  const browser = await getChrome()\n\n  if (!browser) {\n    return {\n      statusCode: 500,\n      body: \"Error launching Chrome\",\n    }\n  }\n\n  try {\n    // call the function that does the real work\n    const response = await workFunction(browser, search)\n\n    return response\n  } catch (err) {\n    console.log(err)\n    return {\n      statusCode: 500,\n      body: \"Error scraping Google\",\n    }\n  }\n}\n"
  },
  {
    "path": "examples/serverless-chrome-example/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2019\",\n    \"module\": \"commonjs\",\n    \"outDir\": \"./dist\",\n    \"strict\": true,\n    \"baseUrl\": \"./\",\n    \"typeRoots\": [\"node_modules/@types\"],\n    \"types\": [\"node\"],\n    \"esModuleInterop\": true,\n    \"inlineSourceMap\": true,\n    \"lib\": [\"ES2019\", \"dom\"]\n  }\n}\n"
  },
  {
    "path": "examples/serverless-data-pipeline-example/.gitignore",
    "content": ".serverless\ndist\n"
  },
  {
    "path": "examples/serverless-data-pipeline-example/package.json",
    "content": "{\n  \"name\": \"serverless-data-pipeline-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple massively distributed adder\",\n  \"main\": \"index.js\",\n  \"repository\": \"https://github.com/Swizec/serverless-handbook\",\n  \"author\": \"Swizec\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"deploy\": \"npm run build && serverless deploy\"\n  },\n  \"engines\": {\n    \"node\": \"12.x\"\n  },\n  \"dependencies\": {\n    \"@types/aws-lambda\": \"^8.10.39\",\n    \"@types/aws-sdk\": \"^2.7.0\",\n    \"@types/node-fetch\": \"^2.5.4\",\n    \"@types/uuid\": \"^3.4.6\",\n    \"aws-lambda\": \"^1.0.4\",\n    \"aws-sdk\": \"^2.597.0\",\n    \"querystring\": \"^0.2.0\",\n    \"simple-dynamodb\": \"^1.0.1\",\n    \"uuid\": \"^3.3.3\"\n  }\n}\n"
  },
  {
    "path": "examples/serverless-data-pipeline-example/serverless.yml",
    "content": "service: serverless-data-pipeline-example\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: dev\n  environment:\n    SUMS_TABLE: ${self:service}-summs-${self:provider.stage}\n    SCRATCHPAD_TABLE: ${self:service}-scratchpad-${self:provider.stage}\n  iamRoleStatements:\n    - Effect: Allow\n      Action:\n        - dynamodb:Query\n        - dynamodb:Scan\n        - dynamodb:GetItem\n        - dynamodb:PutItem\n        - dynamodb:UpdateItem\n        - dynamodb:DeleteItem\n      Resource: \"arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.SUMS_TABLE}\"\n\n    - Effect: Allow\n      Action:\n        - dynamodb:Query\n        - dynamodb:Scan\n        - dynamodb:GetItem\n        - dynamodb:PutItem\n        - dynamodb:UpdateItem\n        - dynamodb:DeleteItem\n      Resource: \"arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.SCRATCHPAD_TABLE}\"\n\n    # all permissions on all SQS queues, what could possibly go wrong\n    - Effect: \"Allow\"\n      Action:\n        - \"sqs:*\"\n      Resource: \"*\"\n\nfunctions:\n  sumArray:\n    handler: dist/sumArray.handler\n    events:\n      - http:\n          path: sumArray\n          method: POST\n          cors: true\n    environment:\n      timesTwoQueueURL:\n        Ref: TimesTwoQueue\n\n  timesTwo:\n    handler: dist/timesTwo.handler\n    events:\n      - sqs:\n          arn:\n            Fn::GetAtt:\n              - TimesTwoQueue\n              - Arn\n          batchSize: 1\n    environment:\n      reduceQueueURL:\n        Ref: ReduceQueue\n\n  reduce:\n    handler: dist/reduce.handler\n    events:\n      - sqs:\n          arn:\n            Fn::GetAtt:\n              - ReduceQueue\n              - Arn\n          # this means we get up to 2 messages per invocation\n          batchSize: 2\n    environment:\n      reduceQueueURL:\n        Ref: ReduceQueue\n\nresources:\n  Resources:\n    SumsTable:\n      Type: \"AWS::DynamoDB::Table\"\n      Properties:\n        AttributeDefinitions:\n          - AttributeName: arrayId\n            AttributeType: S\n        KeySchema:\n          - AttributeName: arrayId\n            KeyType: HASH\n        ProvisionedThroughput:\n          ReadCapacityUnits: 1\n          WriteCapacityUnits: 1\n        TableName: ${self:provider.environment.SUMS_TABLE}\n\n    ScratchpadTable:\n      Type: \"AWS::DynamoDB::Table\"\n      Properties:\n        AttributeDefinitions:\n          - AttributeName: packetId\n            AttributeType: S\n          - AttributeName: arrayId\n            AttributeType: S\n        KeySchema:\n          - AttributeName: packetId\n            KeyType: HASH\n          - AttributeName: arrayId\n            KeyType: RANGE\n        ProvisionedThroughput:\n          ReadCapacityUnits: 1\n          WriteCapacityUnits: 1\n        TableName: ${self:provider.environment.SCRATCHPAD_TABLE}\n\n    TimesTwoQueue:\n      Type: \"AWS::SQS::Queue\"\n      Properties:\n        QueueName: \"TimesTwoQueue-${self:provider.stage}\"\n        VisibilityTimeout: 60\n\n    ReduceQueue:\n      Type: \"AWS::SQS::Queue\"\n      Properties:\n        QueueName: \"ReduceQueue-${self:provider.stage}\"\n        VisibilityTimeout: 60\n        # RedrivePolicy:\n        #   deadLetterTargetArn:\n        #     Fn::GetAtt:\n        #       - UnpackLogDLQueue\n        #       - Arn\n        #   maxReceiveCount: 10\n"
  },
  {
    "path": "examples/serverless-data-pipeline-example/src/reduce.ts",
    "content": "import { SQSEvent, SQSRecord } from \"aws-lambda\"\nimport * as db from \"simple-dynamodb\"\nimport uuidv4 from \"uuid/v4\"\n\nimport { sendSQSMessage, Packet } from \"./utils\"\n\nexport const handler = async (event: SQSEvent) => {\n  // grab messages from queue\n  // depending on batchSize there could be multiple\n  let arrayIds: string[] = event.Records.map((record: SQSRecord) =>\n    JSON.parse(record.body)\n  )\n\n  // process each ID from batch\n  await Promise.all(arrayIds.map(reduceArray))\n}\n\nasync function reduceArray(arrayId: string) {\n  // grab 2 entries from scratchpad table\n  // IRL you'd grab as many as you can cost-effectively process in execution\n  // depends what you're doing\n  const packets = await readPackets(arrayId)\n\n  if (packets.length > 0) {\n    // sum packets together\n    const sum = packets.reduce(\n      (sum: number, packet: Packet) => sum + packet.packetValue,\n      0\n    )\n\n    // add the new item sum to scratchpad table\n    // we do this first so we don't delete rows if it fails\n    const newPacket = {\n      arrayId,\n      packetId: uuidv4(),\n      arrayLength: packets[0].arrayLength,\n      packetValue: sum,\n      packetContains: packets.reduce(\n        (count: number, packet: Packet) => count + packet.packetContains,\n        0\n      ),\n    }\n    await db.updateItem({\n      TableName: process.env.SCRATCHPAD_TABLE!,\n      Key: {\n        arrayId,\n        packetId: uuidv4(),\n      },\n      UpdateExpression:\n        \"SET packetValue = :packetValue, arrayLength = :arrayLength, packetContains = :packetContains\",\n      ExpressionAttributeValues: {\n        \":packetValue\": newPacket.packetValue,\n        \":arrayLength\": newPacket.arrayLength,\n        \":packetContains\": newPacket.packetContains,\n      },\n    })\n\n    // delete the 2 rows we just summed\n    await cleanup(packets)\n\n    // are we done?\n    if (newPacket.packetContains >= newPacket.arrayLength) {\n      // done, save sum to final table\n      await db.updateItem({\n        TableName: process.env.SUMS_TABLE!,\n        Key: {\n          arrayId,\n        },\n        UpdateExpression: \"SET resultSum = :resultSum\",\n        ExpressionAttributeValues: {\n          \":resultSum\": sum,\n        },\n      })\n    } else {\n      // not done, trigger another reduce step\n      await sendSQSMessage(process.env.reduceQueueURL!, arrayId)\n    }\n  }\n}\n\nasync function readPackets(arrayId: string): Promise<Packet[]> {\n  const result = await db.scanItems({\n    TableName: process.env.SCRATCHPAD_TABLE!,\n    FilterExpression: \"#arrayId = :arrayId\",\n    ExpressionAttributeNames: { \"#arrayId\": \"arrayId\" },\n    ExpressionAttributeValues: { \":arrayId\": arrayId },\n    Limit: 2,\n  })\n\n  if (result.Items) {\n    return result.Items as Packet[]\n  } else {\n    return []\n  }\n}\n\nasync function cleanup(packets: Packet[]) {\n  await Promise.all(\n    packets.map((packet) =>\n      db.deleteItem({\n        TableName: process.env.SCRATCHPAD_TABLE!,\n        Key: {\n          arrayId: packet.arrayId,\n          packetId: packet.packetId,\n        },\n      })\n    )\n  )\n}\n"
  },
  {
    "path": "examples/serverless-data-pipeline-example/src/sumArray.ts",
    "content": "import { APIGatewayEvent } from \"aws-lambda\"\nimport uuidv4 from \"uuid/v4\"\nimport { response, sendSQSMessage } from \"./utils\"\n\ninterface APIResponse {\n  statusCode: number\n  body: string\n}\n\nexport const handler = async (event: APIGatewayEvent): Promise<APIResponse> => {\n  const arrayId = uuidv4()\n\n  if (!event.body) {\n    return response(400, {\n      status: \"error\",\n      error: \"Provide a JSON body\",\n    })\n  }\n\n  const array: number[] = JSON.parse(event.body)\n\n  // split array into elements\n  // trigger timesTwo lambda for each entry\n  for (let packetValue of array) {\n    await sendSQSMessage(process.env.timesTwoQueueURL!, {\n      arrayId,\n      packetId: uuidv4(),\n      packetValue,\n      arrayLength: array.length,\n      packetContains: 1,\n    })\n  }\n\n  return response(200, {\n    status: \"success\",\n    array,\n    arrayId,\n  })\n}\n"
  },
  {
    "path": "examples/serverless-data-pipeline-example/src/timesTwo.ts",
    "content": "import { SQSEvent, SQSRecord } from \"aws-lambda\"\nimport { sendSQSMessage, Packet } from \"./utils\"\nimport * as db from \"simple-dynamodb\"\n\nexport const handler = async (event: SQSEvent) => {\n  // grab messages from queue\n  // depending on batchSize there could be multiple\n  let packets: Packet[] = event.Records.map((record: SQSRecord) =>\n    JSON.parse(record.body)\n  )\n\n  // iterate packets and multiply by 2\n  // this would be a more expensive operation usually\n  packets = packets.map((packet) => ({\n    ...packet,\n    packetValue: packet.packetValue * 2,\n  }))\n\n  // store each result in scratchpad table\n  // in theory it's enough to put them on the queue\n  // an intermediary table makes the reduce step easier to implement\n  await Promise.all(\n    packets.map((packet) =>\n      db.updateItem({\n        TableName: process.env.SCRATCHPAD_TABLE!,\n        Key: { arrayId: packet.arrayId, packetId: packet.packetId },\n        UpdateExpression:\n          \"SET packetValue = :packetValue, arrayLength = :arrayLength, packetContains = :packetContains\",\n        ExpressionAttributeValues: {\n          \":packetValue\": packet.packetValue,\n          \":arrayLength\": packet.arrayLength,\n          \":packetContains\": packet.packetContains,\n        },\n      })\n    )\n  )\n\n  // trigger next step in calculation\n  const uniqueArrayIds = Array.from(\n    new Set(packets.map((packet) => packet.arrayId))\n  )\n\n  await Promise.all(\n    uniqueArrayIds.map((arrayId) =>\n      sendSQSMessage(process.env.reduceQueueURL!, arrayId)\n    )\n  )\n\n  return true\n}\n"
  },
  {
    "path": "examples/serverless-data-pipeline-example/src/utils.ts",
    "content": "import AWS from \"aws-sdk\"\n\nexport type Packet = {\n  arrayId: string\n  packetId: string\n  packetValue: number\n  arrayLength: number\n  packetContains: number\n}\n\nexport const sendSQSMessage = async (QueueURL: string, Message: any) => {\n  Message = JSON.stringify(Message)\n\n  console.log(`SQSing ${Message} to ${QueueURL}`)\n\n  return new AWS.SQS()\n    .sendMessage({\n      MessageBody: Message,\n      QueueUrl: QueueURL,\n    })\n    .promise()\n}\n\nexport const fetchSQSMessage = async (QueueURL: string) => {\n  console.log(`fetchSQSing from ${QueueURL}`)\n\n  return new AWS.SQS()\n    .receiveMessage({\n      QueueUrl: QueueURL,\n      WaitTimeSeconds: 1,\n    })\n    .promise()\n}\n\nexport function response(statusCode: number, body: any) {\n  return {\n    statusCode,\n    body: JSON.stringify(body),\n  }\n}\n"
  },
  {
    "path": "examples/serverless-data-pipeline-example/tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"ES2019\",\n        \"module\": \"commonjs\",\n        \"outDir\": \"./dist\",\n        \"strict\": true,\n        \"baseUrl\": \"./\",\n        \"typeRoots\": [\"node_modules/@types\"],\n        \"types\": [\"node\"],\n        \"esModuleInterop\": true,\n        \"inlineSourceMap\": true,\n        \"lib\": [\"ES2019\"]\n    }\n}"
  },
  {
    "path": "examples/serverless-graphql-example/dist/dynamodb.js",
    "content": "\"use strict\";\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n    return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst aws_sdk_1 = __importDefault(require(\"aws-sdk\"));\nconst dynamoDB = new aws_sdk_1.default.DynamoDB.DocumentClient();\nexports.updateItem = async (params) => {\n    const query = {\n        ...params,\n    };\n    return new Promise((resolve, reject) => {\n        dynamoDB.update(query, (err, result) => {\n            if (err) {\n                console.error(err);\n                reject(err);\n            }\n            else {\n                resolve(result);\n            }\n        });\n    });\n};\n// collect all fields in a JSON object into a DynamoDB expression\nexports.buildExpression = (body) => {\n    return Object.keys(body)\n        .map((key) => `${key} = :${key}`)\n        .join(\", \");\n};\nexports.buildAttributes = (body) => {\n    return Object.fromEntries(Object.entries(body).map(([key, value]) => [\n        `:${key}`,\n        typeof value === \"string\" || typeof value === \"number\"\n            ? value\n            : JSON.stringify(value),\n    ]));\n};\nexports.getItem = async (params) => {\n    const query = {\n        ...params,\n    };\n    return new Promise((resolve, reject) => {\n        dynamoDB.get(query, (err, result) => {\n            if (err) {\n                console.error(err);\n                reject(err);\n            }\n            else {\n                resolve(result);\n            }\n        });\n    });\n};\nexports.scanItems = async (params) => {\n    const query = {\n        ...params,\n    };\n    return new Promise((resolve, reject) => {\n        dynamoDB.scan(query, (err, result) => {\n            if (err) {\n                console.error(err);\n                reject(err);\n            }\n            else {\n                resolve(result);\n            }\n        });\n    });\n};\nexports.deleteItem = async (params) => {\n    const query = {\n        ...params,\n    };\n    return new Promise((resolve, reject) => {\n        dynamoDB.delete(query, (err, result) => {\n            if (err) {\n                console.error(err);\n                reject(err);\n            }\n            else {\n                resolve(result);\n            }\n        });\n    });\n};\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZHluYW1vZGIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvZHluYW1vZGIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQSxzREFBeUI7QUFFekIsTUFBTSxRQUFRLEdBQUcsSUFBSSxpQkFBRyxDQUFDLFFBQVEsQ0FBQyxjQUFjLEVBQUUsQ0FBQTtBQXdDckMsUUFBQSxVQUFVLEdBQUcsS0FBSyxFQUM3QixNQUF3QixFQUMrQixFQUFFO0lBQ3pELE1BQU0sS0FBSyxHQUFHO1FBQ1osR0FBRyxNQUFNO0tBQ1YsQ0FBQTtJQUVELE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDckMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDckMsSUFBSSxHQUFHLEVBQUU7Z0JBQ1AsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtnQkFDbEIsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFBO2FBQ1o7aUJBQU07Z0JBQ0wsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFBO2FBQ2hCO1FBQ0gsQ0FBQyxDQUFDLENBQUE7SUFDSixDQUFDLENBQUMsQ0FBQTtBQUNKLENBQUMsQ0FBQTtBQUVELGlFQUFpRTtBQUNwRCxRQUFBLGVBQWUsR0FBRyxDQUFDLElBQVMsRUFBRSxFQUFFO0lBQzNDLE9BQU8sTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7U0FDckIsR0FBRyxDQUFDLENBQUMsR0FBVyxFQUFFLEVBQUUsQ0FBQyxHQUFHLEdBQUcsT0FBTyxHQUFHLEVBQUUsQ0FBQztTQUN4QyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUE7QUFDZixDQUFDLENBQUE7QUFFWSxRQUFBLGVBQWUsR0FBRyxDQUFDLElBQVMsRUFBRSxFQUFFO0lBQzNDLE9BQU8sTUFBTSxDQUFDLFdBQVcsQ0FDdkIsTUFBTSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEdBQUcsRUFBRSxLQUFLLENBQUMsRUFBRSxFQUFFLENBQUM7UUFDekMsSUFBSSxHQUFHLEVBQUU7UUFDVCxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtZQUNwRCxDQUFDLENBQUMsS0FBSztZQUNQLENBQUMsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLEtBQUssQ0FBQztLQUMxQixDQUFDLENBQ0gsQ0FBQTtBQUNILENBQUMsQ0FBQTtBQUVZLFFBQUEsT0FBTyxHQUFHLEtBQUssRUFDMUIsTUFBcUIsRUFDK0IsRUFBRTtJQUN0RCxNQUFNLEtBQUssR0FBRztRQUNaLEdBQUcsTUFBTTtLQUNWLENBQUE7SUFFRCxPQUFPLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxFQUFFO1FBQ3JDLFFBQVEsQ0FBQyxHQUFHLENBQUMsS0FBSyxFQUFFLENBQUMsR0FBRyxFQUFFLE1BQU0sRUFBRSxFQUFFO1lBQ2xDLElBQUksR0FBRyxFQUFFO2dCQUNQLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUE7Z0JBQ2xCLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQTthQUNaO2lCQUFNO2dCQUNMLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQTthQUNoQjtRQUNILENBQUMsQ0FBQyxDQUFBO0lBQ0osQ0FBQyxDQUFDLENBQUE7QUFDSixDQUFDLENBQUE7QUFFWSxRQUFBLFNBQVMsR0FBRyxLQUFLLEVBQzVCLE1BQXVCLEVBQzBCLEVBQUU7SUFDbkQsTUFBTSxLQUFLLEdBQUc7UUFDWixHQUFHLE1BQU07S0FDVixDQUFBO0lBRUQsT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsRUFBRTtRQUNyQyxRQUFRLENBQUMsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDLEdBQUcsRUFBRSxNQUFNLEVBQUUsRUFBRTtZQUNuQyxJQUFJLEdBQUcsRUFBRTtnQkFDUCxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFBO2dCQUNsQixNQUFNLENBQUMsR0FBRyxDQUFDLENBQUE7YUFDWjtpQkFBTTtnQkFDTCxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUE7YUFDaEI7UUFDSCxDQUFDLENBQUMsQ0FBQTtJQUNKLENBQUMsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxDQUFBO0FBRVksUUFBQSxVQUFVLEdBQUcsS0FBSyxFQUM3QixNQUF3QixFQUMrQixFQUFFO0lBQ3pELE1BQU0sS0FBSyxHQUFHO1FBQ1osR0FBRyxNQUFNO0tBQ1YsQ0FBQTtJQUVELE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDckMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDckMsSUFBSSxHQUFHLEVBQUU7Z0JBQ1AsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtnQkFDbEIsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFBO2FBQ1o7aUJBQU07Z0JBQ0wsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFBO2FBQ2hCO1FBQ0gsQ0FBQyxDQUFDLENBQUE7SUFDSixDQUFDLENBQUMsQ0FBQTtBQUNKLENBQUMsQ0FBQSJ9"
  },
  {
    "path": "examples/serverless-graphql-example/dist/graphql.js",
    "content": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst apollo_server_lambda_1 = require(\"apollo-server-lambda\");\nconst queries_1 = require(\"./queries\");\nconst mutations_1 = require(\"./mutations\");\n// this is where we define the shape of our API\nconst schema = apollo_server_lambda_1.gql `\n  type Item {\n    id: String\n    name: String\n    body: String\n    createdAt: String\n    updatedAt: String\n  }\n\n  type Query {\n    item(id: String!): Item\n  }\n\n  type Mutation {\n    updateItem(id: String, name: String, body: String): Item\n    deleteItem(id: String!): Item\n  }\n`;\n// this is where the shape maps to functions\nconst resolvers = {\n    Query: {\n        item: queries_1.item,\n    },\n    Mutation: {\n        updateItem: mutations_1.updateItem,\n        deleteItem: mutations_1.deleteItem,\n    },\n};\nconst server = new apollo_server_lambda_1.ApolloServer({ typeDefs: schema, resolvers });\nexports.handler = server.createHandler({\n    cors: {\n        origin: \"*\",\n        credentials: true,\n    },\n});\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ3JhcGhxbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9ncmFwaHFsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsK0RBQXdEO0FBRXhELHVDQUFnQztBQUNoQywyQ0FBb0Q7QUFFcEQsK0NBQStDO0FBQy9DLE1BQU0sTUFBTSxHQUFHLDBCQUFHLENBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7O0NBaUJqQixDQUFBO0FBRUQsNENBQTRDO0FBQzVDLE1BQU0sU0FBUyxHQUFHO0lBQ2hCLEtBQUssRUFBRTtRQUNMLElBQUksRUFBSixjQUFJO0tBQ0w7SUFDRCxRQUFRLEVBQUU7UUFDUixVQUFVLEVBQVYsc0JBQVU7UUFDVixVQUFVLEVBQVYsc0JBQVU7S0FDWDtDQUNGLENBQUE7QUFFRCxNQUFNLE1BQU0sR0FBRyxJQUFJLG1DQUFZLENBQUMsRUFBRSxRQUFRLEVBQUUsTUFBTSxFQUFFLFNBQVMsRUFBRSxDQUFDLENBQUE7QUFFbkQsUUFBQSxPQUFPLEdBQUcsTUFBTSxDQUFDLGFBQWEsQ0FBQztJQUMxQyxJQUFJLEVBQUU7UUFDSixNQUFNLEVBQUUsR0FBRztRQUNYLFdBQVcsRUFBRSxJQUFJO0tBQ2xCO0NBQ0YsQ0FBQyxDQUFBIn0="
  },
  {
    "path": "examples/serverless-graphql-example/dist/manageItems.js",
    "content": "\"use strict\";\nvar __importStar = (this && this.__importStar) || function (mod) {\n    if (mod && mod.__esModule) return mod;\n    var result = {};\n    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];\n    result[\"default\"] = mod;\n    return result;\n};\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n    return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst db = __importStar(require(\"./dynamodb\"));\nconst v4_1 = __importDefault(require(\"uuid/v4\"));\nfunction response(statusCode, body) {\n    return {\n        statusCode,\n        body: JSON.stringify(body),\n    };\n}\n// fetch using /item/ID\nexports.getItem = async (event) => {\n    const itemId = event.pathParameters ? event.pathParameters.itemId : null;\n    if (!itemId) {\n        return response(404, {\n            status: \"error\",\n            error: \"Item not found\",\n        });\n    }\n    const item = await db.getItem({\n        TableName: process.env.ITEM_TABLE,\n        Key: { itemId },\n    });\n    if (item.Item) {\n        return response(200, {\n            status: \"success\",\n            item: item.Item,\n        });\n    }\n    else {\n        return response(404, {\n            status: \"error\",\n            error: \"Item not found\",\n        });\n    }\n};\n// upsert an item\n// /item or /item/ID\nexports.updateItem = async (event) => {\n    let itemId = event.pathParameters ? event.pathParameters.itemId : v4_1.default();\n    let createdAt = new Date().toISOString();\n    // find item if exists\n    const find = await db.getItem({\n        TableName: process.env.ITEM_TABLE,\n        Key: { itemId },\n    });\n    if (find.Item) {\n        // save createdAt so we don't overwrite on update\n        createdAt = find.Item.createdAt;\n    }\n    else {\n        return response(404, {\n            status: \"error\",\n            error: \"Item not found\",\n        });\n    }\n    if (!event.body) {\n        return response(400, {\n            status: \"error\",\n            error: \"Provide a JSON body\",\n        });\n    }\n    let body = JSON.parse(event.body);\n    if (body.itemId) {\n        // this will confuse DynamoDB, you can't update the key\n        delete body.itemId;\n    }\n    const item = await db.updateItem({\n        TableName: process.env.ITEM_TABLE,\n        Key: { itemId },\n        UpdateExpression: `SET ${db.buildExpression(body)}, createdAt = :createdAt, lastUpdatedAt = :lastUpdatedAt`,\n        ExpressionAttributeValues: {\n            ...db.buildAttributes(body),\n            \":createdAt\": createdAt,\n            \":lastUpdatedAt\": new Date().toISOString(),\n        },\n        ReturnValues: \"ALL_NEW\",\n    });\n    return response(200, {\n        status: \"success\",\n        item: item.Attributes,\n    });\n};\nexports.deleteItem = async (event) => {\n    const itemId = event.pathParameters ? event.pathParameters.itemId : null;\n    if (!itemId) {\n        return response(400, {\n            status: \"error\",\n            error: \"Provide an itemId\",\n        });\n    }\n    // DynamoDB handles deleting already deleted files, no error :)\n    const item = await db.deleteItem({\n        TableName: process.env.ITEM_TABLE,\n        Key: { itemId },\n        ReturnValues: \"ALL_OLD\",\n    });\n    return response(200, {\n        status: \"success\",\n        itemWas: item.Attributes,\n    });\n};\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFuYWdlSXRlbXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvbWFuYWdlSXRlbXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7O0FBRUEsK0NBQWdDO0FBQ2hDLGlEQUE0QjtBQUU1QixTQUFTLFFBQVEsQ0FBQyxVQUFrQixFQUFFLElBQVM7SUFDN0MsT0FBTztRQUNMLFVBQVU7UUFDVixJQUFJLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUM7S0FDM0IsQ0FBQTtBQUNILENBQUM7QUFFRCx1QkFBdUI7QUFDVixRQUFBLE9BQU8sR0FBRyxLQUFLLEVBQUUsS0FBc0IsRUFBd0IsRUFBRTtJQUM1RSxNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFBO0lBRXhFLElBQUksQ0FBQyxNQUFNLEVBQUU7UUFDWCxPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUsZ0JBQWdCO1NBQ3hCLENBQUMsQ0FBQTtLQUNIO0lBRUQsTUFBTSxJQUFJLEdBQUcsTUFBTSxFQUFFLENBQUMsT0FBTyxDQUFDO1FBQzVCLFNBQVMsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLFVBQVc7UUFDbEMsR0FBRyxFQUFFLEVBQUUsTUFBTSxFQUFFO0tBQ2hCLENBQUMsQ0FBQTtJQUVGLElBQUksSUFBSSxDQUFDLElBQUksRUFBRTtRQUNiLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRTtZQUNuQixNQUFNLEVBQUUsU0FBUztZQUNqQixJQUFJLEVBQUUsSUFBSSxDQUFDLElBQUk7U0FDaEIsQ0FBQyxDQUFBO0tBQ0g7U0FBTTtRQUNMLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRTtZQUNuQixNQUFNLEVBQUUsT0FBTztZQUNmLEtBQUssRUFBRSxnQkFBZ0I7U0FDeEIsQ0FBQyxDQUFBO0tBQ0g7QUFDSCxDQUFDLENBQUE7QUFFRCxpQkFBaUI7QUFDakIsb0JBQW9CO0FBQ1AsUUFBQSxVQUFVLEdBQUcsS0FBSyxFQUM3QixLQUFzQixFQUNBLEVBQUU7SUFDeEIsSUFBSSxNQUFNLEdBQUcsS0FBSyxDQUFDLGNBQWMsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLGNBQWMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLFlBQU0sRUFBRSxDQUFBO0lBRTFFLElBQUksU0FBUyxHQUFHLElBQUksSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFLENBQUE7SUFFeEMsc0JBQXNCO0lBQ3RCLE1BQU0sSUFBSSxHQUFHLE1BQU0sRUFBRSxDQUFDLE9BQU8sQ0FBQztRQUM1QixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFXO1FBQ2xDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRTtLQUNoQixDQUFDLENBQUE7SUFFRixJQUFJLElBQUksQ0FBQyxJQUFJLEVBQUU7UUFDYixpREFBaUQ7UUFDakQsU0FBUyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFBO0tBQ2hDO1NBQU07UUFDTCxPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUsZ0JBQWdCO1NBQ3hCLENBQUMsQ0FBQTtLQUNIO0lBRUQsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUU7UUFDZixPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUscUJBQXFCO1NBQzdCLENBQUMsQ0FBQTtLQUNIO0lBRUQsSUFBSSxJQUFJLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUE7SUFFakMsSUFBSSxJQUFJLENBQUMsTUFBTSxFQUFFO1FBQ2YsdURBQXVEO1FBQ3ZELE9BQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQTtLQUNuQjtJQUVELE1BQU0sSUFBSSxHQUFHLE1BQU0sRUFBRSxDQUFDLFVBQVUsQ0FBQztRQUMvQixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFXO1FBQ2xDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRTtRQUNmLGdCQUFnQixFQUFFLE9BQU8sRUFBRSxDQUFDLGVBQWUsQ0FDekMsSUFBSSxDQUNMLDBEQUEwRDtRQUMzRCx5QkFBeUIsRUFBRTtZQUN6QixHQUFHLEVBQUUsQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFDO1lBQzNCLFlBQVksRUFBRSxTQUFTO1lBQ3ZCLGdCQUFnQixFQUFFLElBQUksSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFO1NBQzNDO1FBQ0QsWUFBWSxFQUFFLFNBQVM7S0FDeEIsQ0FBQyxDQUFBO0lBRUYsT0FBTyxRQUFRLENBQUMsR0FBRyxFQUFFO1FBQ25CLE1BQU0sRUFBRSxTQUFTO1FBQ2pCLElBQUksRUFBRSxJQUFJLENBQUMsVUFBVTtLQUN0QixDQUFDLENBQUE7QUFDSixDQUFDLENBQUE7QUFFWSxRQUFBLFVBQVUsR0FBRyxLQUFLLEVBQzdCLEtBQXNCLEVBQ0EsRUFBRTtJQUN4QixNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFBO0lBRXhFLElBQUksQ0FBQyxNQUFNLEVBQUU7UUFDWCxPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUsbUJBQW1CO1NBQzNCLENBQUMsQ0FBQTtLQUNIO0lBRUQsK0RBQStEO0lBQy9ELE1BQU0sSUFBSSxHQUFHLE1BQU0sRUFBRSxDQUFDLFVBQVUsQ0FBQztRQUMvQixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFXO1FBQ2xDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRTtRQUNmLFlBQVksRUFBRSxTQUFTO0tBQ3hCLENBQUMsQ0FBQTtJQUVGLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRTtRQUNuQixNQUFNLEVBQUUsU0FBUztRQUNqQixPQUFPLEVBQUUsSUFBSSxDQUFDLFVBQVU7S0FDekIsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxDQUFBIn0="
  },
  {
    "path": "examples/serverless-graphql-example/dist/mutations.js",
    "content": "\"use strict\";\nvar __importStar = (this && this.__importStar) || function (mod) {\n    if (mod && mod.__esModule) return mod;\n    var result = {};\n    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];\n    result[\"default\"] = mod;\n    return result;\n};\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n    return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst db = __importStar(require(\"simple-dynamodb\"));\nconst v4_1 = __importDefault(require(\"uuid/v4\"));\nfunction remapProps(item) {\n    return {\n        ...item,\n        id: item.itemId,\n        name: item.itemName,\n    };\n}\n// upsert an item\n// item(name, ...) or item(id, name, ...)\nexports.updateItem = async (_, args) => {\n    let itemId = args.id ? args.id : v4_1.default();\n    let createdAt = new Date().toISOString();\n    // find item if exists\n    if (args.id) {\n        const find = await db.getItem({\n            TableName: process.env.ITEM_TABLE,\n            Key: { itemId },\n        });\n        if (find.Item) {\n            // save createdAt so we don't overwrite on update\n            createdAt = find.Item.createdAt;\n        }\n        else {\n            throw \"Item not found\";\n        }\n    }\n    const updateValues = {\n        itemName: args.name,\n        body: args.body,\n    };\n    const item = await db.updateItem({\n        TableName: process.env.ITEM_TABLE,\n        Key: { itemId },\n        UpdateExpression: `SET ${db.buildExpression(updateValues)}, createdAt = :createdAt, updatedAt = :updatedAt`,\n        ExpressionAttributeValues: {\n            ...db.buildAttributes(updateValues),\n            \":createdAt\": createdAt,\n            \":updatedAt\": new Date().toISOString(),\n        },\n        ReturnValues: \"ALL_NEW\",\n    });\n    return remapProps(item.Attributes);\n};\nexports.deleteItem = async (_, args) => {\n    // DynamoDB handles deleting already deleted files, no error :)\n    const item = await db.deleteItem({\n        TableName: process.env.ITEM_TABLE,\n        Key: {\n            itemId: args.id,\n        },\n        ReturnValues: \"ALL_OLD\",\n    });\n    return remapProps(item.Attributes);\n};\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibXV0YXRpb25zLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL211dGF0aW9ucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7QUFBQSxvREFBcUM7QUFDckMsaURBQTRCO0FBUTVCLFNBQVMsVUFBVSxDQUFDLElBQVM7SUFDM0IsT0FBTztRQUNMLEdBQUcsSUFBSTtRQUNQLEVBQUUsRUFBRSxJQUFJLENBQUMsTUFBTTtRQUNmLElBQUksRUFBRSxJQUFJLENBQUMsUUFBUTtLQUNwQixDQUFBO0FBQ0gsQ0FBQztBQUVELGlCQUFpQjtBQUNqQix5Q0FBeUM7QUFDNUIsUUFBQSxVQUFVLEdBQUcsS0FBSyxFQUFFLENBQU0sRUFBRSxJQUFjLEVBQUUsRUFBRTtJQUN6RCxJQUFJLE1BQU0sR0FBRyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxZQUFNLEVBQUUsQ0FBQTtJQUV6QyxJQUFJLFNBQVMsR0FBRyxJQUFJLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRSxDQUFBO0lBRXhDLHNCQUFzQjtJQUN0QixJQUFJLElBQUksQ0FBQyxFQUFFLEVBQUU7UUFDWCxNQUFNLElBQUksR0FBRyxNQUFNLEVBQUUsQ0FBQyxPQUFPLENBQUM7WUFDNUIsU0FBUyxFQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsVUFBVztZQUNsQyxHQUFHLEVBQUUsRUFBRSxNQUFNLEVBQUU7U0FDaEIsQ0FBQyxDQUFBO1FBRUYsSUFBSSxJQUFJLENBQUMsSUFBSSxFQUFFO1lBQ2IsaURBQWlEO1lBQ2pELFNBQVMsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQTtTQUNoQzthQUFNO1lBQ0wsTUFBTSxnQkFBZ0IsQ0FBQTtTQUN2QjtLQUNGO0lBRUQsTUFBTSxZQUFZLEdBQUc7UUFDbkIsUUFBUSxFQUFFLElBQUksQ0FBQyxJQUFJO1FBQ25CLElBQUksRUFBRSxJQUFJLENBQUMsSUFBSTtLQUNoQixDQUFBO0lBRUQsTUFBTSxJQUFJLEdBQUcsTUFBTSxFQUFFLENBQUMsVUFBVSxDQUFDO1FBQy9CLFNBQVMsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLFVBQVc7UUFDbEMsR0FBRyxFQUFFLEVBQUUsTUFBTSxFQUFFO1FBQ2YsZ0JBQWdCLEVBQUUsT0FBTyxFQUFFLENBQUMsZUFBZSxDQUN6QyxZQUFZLENBQ2Isa0RBQWtEO1FBQ25ELHlCQUF5QixFQUFFO1lBQ3pCLEdBQUcsRUFBRSxDQUFDLGVBQWUsQ0FBQyxZQUFZLENBQUM7WUFDbkMsWUFBWSxFQUFFLFNBQVM7WUFDdkIsWUFBWSxFQUFFLElBQUksSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFO1NBQ3ZDO1FBQ0QsWUFBWSxFQUFFLFNBQVM7S0FDeEIsQ0FBQyxDQUFBO0lBRUYsT0FBTyxVQUFVLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxDQUFBO0FBQ3BDLENBQUMsQ0FBQTtBQUVZLFFBQUEsVUFBVSxHQUFHLEtBQUssRUFBRSxDQUFNLEVBQUUsSUFBb0IsRUFBRSxFQUFFO0lBQy9ELCtEQUErRDtJQUMvRCxNQUFNLElBQUksR0FBRyxNQUFNLEVBQUUsQ0FBQyxVQUFVLENBQUM7UUFDL0IsU0FBUyxFQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsVUFBVztRQUNsQyxHQUFHLEVBQUU7WUFDSCxNQUFNLEVBQUUsSUFBSSxDQUFDLEVBQUU7U0FDaEI7UUFDRCxZQUFZLEVBQUUsU0FBUztLQUN4QixDQUFDLENBQUE7SUFFRixPQUFPLFVBQVUsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLENBQUE7QUFDcEMsQ0FBQyxDQUFBIn0="
  },
  {
    "path": "examples/serverless-graphql-example/dist/queries.js",
    "content": "\"use strict\";\nvar __importStar = (this && this.__importStar) || function (mod) {\n    if (mod && mod.__esModule) return mod;\n    var result = {};\n    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];\n    result[\"default\"] = mod;\n    return result;\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst db = __importStar(require(\"simple-dynamodb\"));\nfunction remapProps(item) {\n    return {\n        ...item,\n        id: item.itemId,\n        name: item.itemName,\n    };\n}\n// fetch using item(id: String)\nexports.item = async (_, args) => {\n    const item = await db.getItem({\n        TableName: process.env.ITEM_TABLE,\n        Key: {\n            itemId: args.id,\n        },\n    });\n    return remapProps(item.Item);\n};\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicXVlcmllcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9xdWVyaWVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7OztBQUFBLG9EQUFxQztBQUVyQyxTQUFTLFVBQVUsQ0FBQyxJQUFTO0lBQzNCLE9BQU87UUFDTCxHQUFHLElBQUk7UUFDUCxFQUFFLEVBQUUsSUFBSSxDQUFDLE1BQU07UUFDZixJQUFJLEVBQUUsSUFBSSxDQUFDLFFBQVE7S0FDcEIsQ0FBQTtBQUNILENBQUM7QUFFRCwrQkFBK0I7QUFDbEIsUUFBQSxJQUFJLEdBQUcsS0FBSyxFQUFFLENBQU0sRUFBRSxJQUFvQixFQUFFLEVBQUU7SUFDekQsTUFBTSxJQUFJLEdBQUcsTUFBTSxFQUFFLENBQUMsT0FBTyxDQUFDO1FBQzVCLFNBQVMsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLFVBQVc7UUFDbEMsR0FBRyxFQUFFO1lBQ0gsTUFBTSxFQUFFLElBQUksQ0FBQyxFQUFFO1NBQ2hCO0tBQ0YsQ0FBQyxDQUFBO0lBRUYsT0FBTyxVQUFVLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFBO0FBQzlCLENBQUMsQ0FBQSJ9"
  },
  {
    "path": "examples/serverless-graphql-example/package.json",
    "content": "{\n  \"name\": \"serverless-rest-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple CRUD example using serverless\",\n  \"main\": \"index.js\",\n  \"repository\": \"https://github.com/Swizec/serverless-handbook\",\n  \"author\": \"Swizec\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"deploy\": \"npm run build && serverless deploy\"\n  },\n  \"engines\": {\n    \"node\": \"12.x\"\n  },\n  \"dependencies\": {\n    \"@types/aws-lambda\": \"^8.10.39\",\n    \"@types/aws-sdk\": \"^2.7.0\",\n    \"@types/node-fetch\": \"^2.5.4\",\n    \"@types/uuid\": \"^3.4.6\",\n    \"apollo-server-lambda\": \"^2.12.0\",\n    \"aws-lambda\": \"^1.0.4\",\n    \"aws-sdk\": \"^2.597.0\",\n    \"querystring\": \"^0.2.0\",\n    \"simple-dynamodb\": \"^1.0.0\",\n    \"uuid\": \"^3.3.3\"\n  }\n}\n"
  },
  {
    "path": "examples/serverless-graphql-example/serverless.yml",
    "content": "service: serverless-graphql-example\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: dev\n  environment:\n    ITEM_TABLE: ${self:service}-items-${self:provider.stage}\n  iamRoleStatements:\n    - Effect: Allow\n      Action:\n        - dynamodb:Query\n        - dynamodb:Scan\n        - dynamodb:GetItem\n        - dynamodb:PutItem\n        - dynamodb:UpdateItem\n        - dynamodb:DeleteItem\n      Resource: \"arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.ITEM_TABLE}\"\n\nfunctions:\n  graphql:\n    handler: dist/graphql.handler\n    events:\n      - http:\n          path: graphql\n          method: GET\n          cors: true\n      - http:\n          path: graphql\n          method: POST\n          cors: true\n\nresources:\n  Resources:\n    UsersTable:\n      Type: \"AWS::DynamoDB::Table\"\n      Properties:\n        AttributeDefinitions:\n          - AttributeName: itemId\n            AttributeType: S\n        KeySchema:\n          - AttributeName: itemId\n            KeyType: HASH\n        ProvisionedThroughput:\n          ReadCapacityUnits: 1\n          WriteCapacityUnits: 1\n        TableName: ${self:provider.environment.ITEM_TABLE}\n"
  },
  {
    "path": "examples/serverless-graphql-example/src/graphql.ts",
    "content": "import { ApolloServer, gql } from \"apollo-server-lambda\"\n\nimport { item } from \"./queries\"\nimport { updateItem, deleteItem } from \"./mutations\"\n\n// this is where we define the shape of our API\nconst schema = gql`\n  type Item {\n    id: String\n    name: String\n    body: String\n    createdAt: String\n    updatedAt: String\n  }\n\n  type Query {\n    item(id: String!): Item\n  }\n\n  type Mutation {\n    updateItem(id: String, name: String, body: String): Item\n    deleteItem(id: String!): Item\n  }\n`\n\n// this is where the shape maps to functions\nconst resolvers = {\n  Query: {\n    item,\n  },\n  Mutation: {\n    updateItem,\n    deleteItem,\n  },\n}\n\nconst server = new ApolloServer({ typeDefs: schema, resolvers })\n\nexport const handler = server.createHandler({\n  cors: {\n    origin: \"*\", // for security in production, lock this to your real endpoints\n    credentials: true,\n  },\n})\n"
  },
  {
    "path": "examples/serverless-graphql-example/src/mutations.ts",
    "content": "import * as db from \"simple-dynamodb\"\nimport uuidv4 from \"uuid/v4\"\n\ntype ItemArgs = {\n  id: string\n  name: string\n  body: string\n}\n\nfunction remapProps(item: any) {\n  return {\n    ...item,\n    id: item.itemId,\n    name: item.itemName,\n  }\n}\n\n// upsert an item\n// item(name, ...) or item(id, name, ...)\nexport const updateItem = async (_: any, args: ItemArgs) => {\n  let itemId = args.id ? args.id : uuidv4()\n\n  let createdAt = new Date().toISOString()\n\n  // find item if exists\n  if (args.id) {\n    const find = await db.getItem({\n      TableName: process.env.ITEM_TABLE!,\n      Key: { itemId },\n    })\n\n    if (find.Item) {\n      // save createdAt so we don't overwrite on update\n      createdAt = find.Item.createdAt\n    } else {\n      throw \"Item not found\"\n    }\n  }\n\n  const updateValues = {\n    itemName: args.name,\n    body: args.body,\n  }\n\n  const item = await db.updateItem({\n    TableName: process.env.ITEM_TABLE!,\n    Key: { itemId },\n    UpdateExpression: `SET ${db.buildExpression(\n      updateValues\n    )}, createdAt = :createdAt, updatedAt = :updatedAt`,\n    ExpressionAttributeValues: {\n      ...db.buildAttributes(updateValues),\n      \":createdAt\": createdAt,\n      \":updatedAt\": new Date().toISOString(),\n    },\n    ReturnValues: \"ALL_NEW\",\n  })\n\n  return remapProps(item.Attributes)\n}\n\nexport const deleteItem = async (_: any, args: { id: string }) => {\n  // DynamoDB handles deleting already deleted files, no error :)\n  const item = await db.deleteItem({\n    TableName: process.env.ITEM_TABLE!,\n    Key: {\n      itemId: args.id,\n    },\n    ReturnValues: \"ALL_OLD\",\n  })\n\n  return remapProps(item.Attributes)\n}\n"
  },
  {
    "path": "examples/serverless-graphql-example/src/queries.ts",
    "content": "import * as db from \"simple-dynamodb\"\n\nfunction remapProps(item: any) {\n  return {\n    ...item,\n    id: item.itemId,\n    name: item.itemName,\n  }\n}\n\n// fetch using item(id: String)\nexport const item = async (_: any, args: { id: string }) => {\n  const item = await db.getItem({\n    TableName: process.env.ITEM_TABLE!,\n    Key: {\n      itemId: args.id,\n    },\n  })\n\n  return remapProps(item.Item)\n}\n"
  },
  {
    "path": "examples/serverless-graphql-example/src/types.d.ts",
    "content": "export interface APIResponse {\n  statusCode: number\n  body: string\n}\n"
  },
  {
    "path": "examples/serverless-graphql-example/tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"ES2019\",\n        \"module\": \"commonjs\",\n        \"outDir\": \"./dist\",\n        \"strict\": true,\n        \"baseUrl\": \"./\",\n        \"typeRoots\": [\"node_modules/@types\"],\n        \"types\": [\"node\"],\n        \"esModuleInterop\": true,\n        \"inlineSourceMap\": true,\n        \"lib\": [\"ES2019\"]\n    }\n}"
  },
  {
    "path": "examples/serverless-rest-example/dist/dynamodb.js",
    "content": "\"use strict\";\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n    return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst aws_sdk_1 = __importDefault(require(\"aws-sdk\"));\nconst dynamoDB = new aws_sdk_1.default.DynamoDB.DocumentClient();\nexports.updateItem = async (params) => {\n    const query = {\n        ...params,\n    };\n    return new Promise((resolve, reject) => {\n        dynamoDB.update(query, (err, result) => {\n            if (err) {\n                console.error(err);\n                reject(err);\n            }\n            else {\n                resolve(result);\n            }\n        });\n    });\n};\n// collect all fields in a JSON object into a DynamoDB expression\nexports.buildExpression = (body) => {\n    return Object.keys(body)\n        .map((key) => `${key} = :${key}`)\n        .join(\", \");\n};\nexports.buildAttributes = (body) => {\n    return Object.fromEntries(Object.entries(body).map(([key, value]) => [\n        `:${key}`,\n        typeof value === \"string\" || typeof value === \"number\"\n            ? value\n            : JSON.stringify(value),\n    ]));\n};\nexports.getItem = async (params) => {\n    const query = {\n        ...params,\n    };\n    return new Promise((resolve, reject) => {\n        dynamoDB.get(query, (err, result) => {\n            if (err) {\n                console.error(err);\n                reject(err);\n            }\n            else {\n                resolve(result);\n            }\n        });\n    });\n};\nexports.scanItems = async (params) => {\n    const query = {\n        ...params,\n    };\n    return new Promise((resolve, reject) => {\n        dynamoDB.scan(query, (err, result) => {\n            if (err) {\n                console.error(err);\n                reject(err);\n            }\n            else {\n                resolve(result);\n            }\n        });\n    });\n};\nexports.deleteItem = async (params) => {\n    const query = {\n        ...params,\n    };\n    return new Promise((resolve, reject) => {\n        dynamoDB.delete(query, (err, result) => {\n            if (err) {\n                console.error(err);\n                reject(err);\n            }\n            else {\n                resolve(result);\n            }\n        });\n    });\n};\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZHluYW1vZGIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvZHluYW1vZGIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQSxzREFBeUI7QUFFekIsTUFBTSxRQUFRLEdBQUcsSUFBSSxpQkFBRyxDQUFDLFFBQVEsQ0FBQyxjQUFjLEVBQUUsQ0FBQTtBQXdDckMsUUFBQSxVQUFVLEdBQUcsS0FBSyxFQUM3QixNQUF3QixFQUMrQixFQUFFO0lBQ3pELE1BQU0sS0FBSyxHQUFHO1FBQ1osR0FBRyxNQUFNO0tBQ1YsQ0FBQTtJQUVELE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDckMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDckMsSUFBSSxHQUFHLEVBQUU7Z0JBQ1AsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtnQkFDbEIsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFBO2FBQ1o7aUJBQU07Z0JBQ0wsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFBO2FBQ2hCO1FBQ0gsQ0FBQyxDQUFDLENBQUE7SUFDSixDQUFDLENBQUMsQ0FBQTtBQUNKLENBQUMsQ0FBQTtBQUVELGlFQUFpRTtBQUNwRCxRQUFBLGVBQWUsR0FBRyxDQUFDLElBQVMsRUFBRSxFQUFFO0lBQzNDLE9BQU8sTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7U0FDckIsR0FBRyxDQUFDLENBQUMsR0FBVyxFQUFFLEVBQUUsQ0FBQyxHQUFHLEdBQUcsT0FBTyxHQUFHLEVBQUUsQ0FBQztTQUN4QyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUE7QUFDZixDQUFDLENBQUE7QUFFWSxRQUFBLGVBQWUsR0FBRyxDQUFDLElBQVMsRUFBRSxFQUFFO0lBQzNDLE9BQU8sTUFBTSxDQUFDLFdBQVcsQ0FDdkIsTUFBTSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEdBQUcsRUFBRSxLQUFLLENBQUMsRUFBRSxFQUFFLENBQUM7UUFDekMsSUFBSSxHQUFHLEVBQUU7UUFDVCxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtZQUNwRCxDQUFDLENBQUMsS0FBSztZQUNQLENBQUMsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLEtBQUssQ0FBQztLQUMxQixDQUFDLENBQ0gsQ0FBQTtBQUNILENBQUMsQ0FBQTtBQUVZLFFBQUEsT0FBTyxHQUFHLEtBQUssRUFDMUIsTUFBcUIsRUFDK0IsRUFBRTtJQUN0RCxNQUFNLEtBQUssR0FBRztRQUNaLEdBQUcsTUFBTTtLQUNWLENBQUE7SUFFRCxPQUFPLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxFQUFFO1FBQ3JDLFFBQVEsQ0FBQyxHQUFHLENBQUMsS0FBSyxFQUFFLENBQUMsR0FBRyxFQUFFLE1BQU0sRUFBRSxFQUFFO1lBQ2xDLElBQUksR0FBRyxFQUFFO2dCQUNQLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUE7Z0JBQ2xCLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQTthQUNaO2lCQUFNO2dCQUNMLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQTthQUNoQjtRQUNILENBQUMsQ0FBQyxDQUFBO0lBQ0osQ0FBQyxDQUFDLENBQUE7QUFDSixDQUFDLENBQUE7QUFFWSxRQUFBLFNBQVMsR0FBRyxLQUFLLEVBQzVCLE1BQXVCLEVBQzBCLEVBQUU7SUFDbkQsTUFBTSxLQUFLLEdBQUc7UUFDWixHQUFHLE1BQU07S0FDVixDQUFBO0lBRUQsT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsRUFBRTtRQUNyQyxRQUFRLENBQUMsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDLEdBQUcsRUFBRSxNQUFNLEVBQUUsRUFBRTtZQUNuQyxJQUFJLEdBQUcsRUFBRTtnQkFDUCxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFBO2dCQUNsQixNQUFNLENBQUMsR0FBRyxDQUFDLENBQUE7YUFDWjtpQkFBTTtnQkFDTCxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUE7YUFDaEI7UUFDSCxDQUFDLENBQUMsQ0FBQTtJQUNKLENBQUMsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxDQUFBO0FBRVksUUFBQSxVQUFVLEdBQUcsS0FBSyxFQUM3QixNQUF3QixFQUMrQixFQUFFO0lBQ3pELE1BQU0sS0FBSyxHQUFHO1FBQ1osR0FBRyxNQUFNO0tBQ1YsQ0FBQTtJQUVELE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDckMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDckMsSUFBSSxHQUFHLEVBQUU7Z0JBQ1AsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtnQkFDbEIsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFBO2FBQ1o7aUJBQU07Z0JBQ0wsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFBO2FBQ2hCO1FBQ0gsQ0FBQyxDQUFDLENBQUE7SUFDSixDQUFDLENBQUMsQ0FBQTtBQUNKLENBQUMsQ0FBQSJ9"
  },
  {
    "path": "examples/serverless-rest-example/dist/manageItems.js",
    "content": "\"use strict\";\nvar __importStar = (this && this.__importStar) || function (mod) {\n    if (mod && mod.__esModule) return mod;\n    var result = {};\n    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];\n    result[\"default\"] = mod;\n    return result;\n};\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n    return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst db = __importStar(require(\"./dynamodb\"));\nconst v4_1 = __importDefault(require(\"uuid/v4\"));\nfunction response(statusCode, body) {\n    return {\n        statusCode,\n        body: JSON.stringify(body),\n    };\n}\n// fetch using /item/ID\nexports.getItem = async (event) => {\n    const itemId = event.pathParameters ? event.pathParameters.itemId : \"\";\n    const item = await db.getItem({\n        TableName: process.env.ITEM_TABLE,\n        Key: { itemId },\n    });\n    if (item.Item) {\n        return response(200, {\n            status: \"success\",\n            item: item.Item,\n        });\n    }\n    else {\n        return response(404, {\n            status: \"error\",\n            error: \"Item not found\",\n        });\n    }\n};\n// upsert an item\n// /item or /item/ID\nexports.updateItem = async (event) => {\n    let itemId = event.pathParameters ? event.pathParameters.itemId : v4_1.default();\n    let createdAt = new Date().toISOString();\n    if (event.pathParameters && event.pathParameters.itemId) {\n        // find item if exists\n        const find = await db.getItem({\n            TableName: process.env.ITEM_TABLE,\n            Key: { itemId },\n        });\n        if (find.Item) {\n            // save createdAt so we don't overwrite on update\n            createdAt = find.Item.createdAt;\n        }\n        else {\n            return response(404, {\n                status: \"error\",\n                error: `Item not found ${event.pathParameters.itemId}`,\n            });\n        }\n    }\n    if (!event.body) {\n        return response(400, {\n            status: \"error\",\n            error: \"Provide a JSON body\",\n        });\n    }\n    let body = JSON.parse(event.body);\n    if (body.itemId) {\n        // this will confuse DynamoDB, you can't update the key\n        delete body.itemId;\n    }\n    const item = await db.updateItem({\n        TableName: process.env.ITEM_TABLE,\n        Key: { itemId },\n        UpdateExpression: `SET ${db.buildExpression(body)}, createdAt = :createdAt, lastUpdatedAt = :lastUpdatedAt`,\n        ExpressionAttributeValues: {\n            ...db.buildAttributes(body),\n            \":createdAt\": createdAt,\n            \":lastUpdatedAt\": new Date().toISOString(),\n        },\n        ReturnValues: \"ALL_NEW\",\n    });\n    return response(200, {\n        status: \"success\",\n        item: item.Attributes,\n    });\n};\nexports.deleteItem = async (event) => {\n    const itemId = event.pathParameters ? event.pathParameters.itemId : null;\n    if (!itemId) {\n        return response(400, {\n            status: \"error\",\n            error: \"Provide an itemId\",\n        });\n    }\n    // DynamoDB handles deleting already deleted files, no error :)\n    const item = await db.deleteItem({\n        TableName: process.env.ITEM_TABLE,\n        Key: { itemId },\n        ReturnValues: \"ALL_OLD\",\n    });\n    return response(200, {\n        status: \"success\",\n        itemWas: item.Attributes,\n    });\n};\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFuYWdlSXRlbXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvbWFuYWdlSXRlbXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7O0FBRUEsK0NBQWdDO0FBQ2hDLGlEQUE0QjtBQUU1QixTQUFTLFFBQVEsQ0FBQyxVQUFrQixFQUFFLElBQVM7SUFDN0MsT0FBTztRQUNMLFVBQVU7UUFDVixJQUFJLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUM7S0FDM0IsQ0FBQTtBQUNILENBQUM7QUFFRCx1QkFBdUI7QUFDVixRQUFBLE9BQU8sR0FBRyxLQUFLLEVBQUUsS0FBc0IsRUFBd0IsRUFBRTtJQUM1RSxNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFBO0lBRXRFLE1BQU0sSUFBSSxHQUFHLE1BQU0sRUFBRSxDQUFDLE9BQU8sQ0FBQztRQUM1QixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFXO1FBQ2xDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRTtLQUNoQixDQUFDLENBQUE7SUFFRixJQUFJLElBQUksQ0FBQyxJQUFJLEVBQUU7UUFDYixPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLFNBQVM7WUFDakIsSUFBSSxFQUFFLElBQUksQ0FBQyxJQUFJO1NBQ2hCLENBQUMsQ0FBQTtLQUNIO1NBQU07UUFDTCxPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUsZ0JBQWdCO1NBQ3hCLENBQUMsQ0FBQTtLQUNIO0FBQ0gsQ0FBQyxDQUFBO0FBRUQsaUJBQWlCO0FBQ2pCLG9CQUFvQjtBQUNQLFFBQUEsVUFBVSxHQUFHLEtBQUssRUFDN0IsS0FBc0IsRUFDQSxFQUFFO0lBQ3hCLElBQUksTUFBTSxHQUFHLEtBQUssQ0FBQyxjQUFjLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxjQUFjLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxZQUFNLEVBQUUsQ0FBQTtJQUUxRSxJQUFJLFNBQVMsR0FBRyxJQUFJLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRSxDQUFBO0lBRXhDLElBQUksS0FBSyxDQUFDLGNBQWMsSUFBSSxLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sRUFBRTtRQUN2RCxzQkFBc0I7UUFDdEIsTUFBTSxJQUFJLEdBQUcsTUFBTSxFQUFFLENBQUMsT0FBTyxDQUFDO1lBQzVCLFNBQVMsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLFVBQVc7WUFDbEMsR0FBRyxFQUFFLEVBQUUsTUFBTSxFQUFFO1NBQ2hCLENBQUMsQ0FBQTtRQUNGLElBQUksSUFBSSxDQUFDLElBQUksRUFBRTtZQUNiLGlEQUFpRDtZQUNqRCxTQUFTLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUE7U0FDaEM7YUFBTTtZQUNMLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRTtnQkFDbkIsTUFBTSxFQUFFLE9BQU87Z0JBQ2YsS0FBSyxFQUFFLGtCQUFrQixLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sRUFBRTthQUN2RCxDQUFDLENBQUE7U0FDSDtLQUNGO0lBRUQsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUU7UUFDZixPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUscUJBQXFCO1NBQzdCLENBQUMsQ0FBQTtLQUNIO0lBRUQsSUFBSSxJQUFJLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUE7SUFFakMsSUFBSSxJQUFJLENBQUMsTUFBTSxFQUFFO1FBQ2YsdURBQXVEO1FBQ3ZELE9BQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQTtLQUNuQjtJQUVELE1BQU0sSUFBSSxHQUFHLE1BQU0sRUFBRSxDQUFDLFVBQVUsQ0FBQztRQUMvQixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFXO1FBQ2xDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRTtRQUNmLGdCQUFnQixFQUFFLE9BQU8sRUFBRSxDQUFDLGVBQWUsQ0FDekMsSUFBSSxDQUNMLDBEQUEwRDtRQUMzRCx5QkFBeUIsRUFBRTtZQUN6QixHQUFHLEVBQUUsQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFDO1lBQzNCLFlBQVksRUFBRSxTQUFTO1lBQ3ZCLGdCQUFnQixFQUFFLElBQUksSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFO1NBQzNDO1FBQ0QsWUFBWSxFQUFFLFNBQVM7S0FDeEIsQ0FBQyxDQUFBO0lBRUYsT0FBTyxRQUFRLENBQUMsR0FBRyxFQUFFO1FBQ25CLE1BQU0sRUFBRSxTQUFTO1FBQ2pCLElBQUksRUFBRSxJQUFJLENBQUMsVUFBVTtLQUN0QixDQUFDLENBQUE7QUFDSixDQUFDLENBQUE7QUFFWSxRQUFBLFVBQVUsR0FBRyxLQUFLLEVBQzdCLEtBQXNCLEVBQ0EsRUFBRTtJQUN4QixNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFBO0lBRXhFLElBQUksQ0FBQyxNQUFNLEVBQUU7UUFDWCxPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUsbUJBQW1CO1NBQzNCLENBQUMsQ0FBQTtLQUNIO0lBRUQsK0RBQStEO0lBQy9ELE1BQU0sSUFBSSxHQUFHLE1BQU0sRUFBRSxDQUFDLFVBQVUsQ0FBQztRQUMvQixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFXO1FBQ2xDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRTtRQUNmLFlBQVksRUFBRSxTQUFTO0tBQ3hCLENBQUMsQ0FBQTtJQUVGLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRTtRQUNuQixNQUFNLEVBQUUsU0FBUztRQUNqQixPQUFPLEVBQUUsSUFBSSxDQUFDLFVBQVU7S0FDekIsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxDQUFBIn0="
  },
  {
    "path": "examples/serverless-rest-example/package.json",
    "content": "{\n  \"name\": \"serverless-rest-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple CRUD example using serverless\",\n  \"main\": \"index.js\",\n  \"repository\": \"https://github.com/Swizec/serverless-handbook\",\n  \"author\": \"Swizec\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"deploy\": \"npm run build && serverless deploy\"\n  },\n  \"engines\": {\n    \"node\": \"12.x || 14.x\"\n  },\n  \"dependencies\": {\n    \"@types/aws-lambda\": \"^8.10.39\",\n    \"@types/aws-sdk\": \"^2.7.0\",\n    \"@types/node-fetch\": \"^2.5.4\",\n    \"@types/uuid\": \"^3.4.6\",\n    \"aws-lambda\": \"^1.0.4\",\n    \"aws-sdk\": \"^2.597.0\",\n    \"prettier\": \"^2.2.1\",\n    \"querystring\": \"^0.2.0\",\n    \"uuid\": \"^3.3.3\"\n  }\n}\n"
  },
  {
    "path": "examples/serverless-rest-example/serverless.yml",
    "content": "service: serverless-rest-example\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  region: us-east-1\n  stage: dev\n  environment:\n    ITEM_TABLE: ${self:service}-items-${self:provider.stage}\n  iamRoleStatements:\n    - Effect: Allow\n      Action:\n        - dynamodb:Query\n        - dynamodb:Scan\n        - dynamodb:GetItem\n        - dynamodb:PutItem\n        - dynamodb:UpdateItem\n        - dynamodb:DeleteItem\n      Resource: \"arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.ITEM_TABLE}\"\n\nfunctions:\n  getItems:\n    handler: dist/manageItems.getItem\n    events:\n      - http:\n          path: item/{itemId}\n          method: GET\n          cors: true\n  updateItems:\n    handler: dist/manageItems.updateItem\n    events:\n      - http:\n          path: item\n          method: POST\n          cors: true\n      - http:\n          path: item/{itemId}\n          method: POST\n          cors: true\n  deleteItems:\n    handler: dist/manageItems.deleteItem\n    events:\n      - http:\n          path: item/{itemId}\n          method: DELETE\n          cors: true\n\nresources:\n  Resources:\n    UsersTable:\n      Type: \"AWS::DynamoDB::Table\"\n      Properties:\n        AttributeDefinitions:\n          - AttributeName: itemId\n            AttributeType: S\n        KeySchema:\n          - AttributeName: itemId\n            KeyType: HASH\n        ProvisionedThroughput:\n          ReadCapacityUnits: 1\n          WriteCapacityUnits: 1\n        TableName: ${self:provider.environment.ITEM_TABLE}\n"
  },
  {
    "path": "examples/serverless-rest-example/src/dynamodb.ts",
    "content": "import AWS from \"aws-sdk\"\n\nconst dynamoDB = new AWS.DynamoDB.DocumentClient()\n\ninterface UpdateItemParams {\n  TableName: string\n  Key: {\n    [key: string]: string\n  }\n  UpdateExpression: string\n  ExpressionAttributeValues: {\n    [key: string]: string | number | undefined | null\n  }\n  ReturnValues?: string\n}\n\ninterface GetItemParams {\n  TableName: string\n  Key: {\n    [key: string]: string\n  }\n}\n\ninterface DeleteItemParams {\n  TableName: string\n  Key: {\n    [key: string]: string\n  }\n  ReturnValues?: string\n}\n\ninterface ScanItemsParams {\n  TableName: string\n  FilterExpression?: string\n  ExpressionAttributeNames?: {\n    [key: string]: string\n  }\n  ExpressionAttributeValues?: {\n    [key: string]: string | number | undefined | null\n  }\n}\n\nexport const updateItem = async (\n  params: UpdateItemParams\n): Promise<AWS.DynamoDB.DocumentClient.UpdateItemOutput> => {\n  const query = {\n    ...params,\n  }\n\n  return new Promise((resolve, reject) => {\n    dynamoDB.update(query, (err, result) => {\n      if (err) {\n        console.error(err)\n        reject(err)\n      } else {\n        resolve(result)\n      }\n    })\n  })\n}\n\n// collect all fields in a JSON object into a DynamoDB expression\nexport const buildExpression = (body: any) => {\n  return Object.keys(body)\n    .map((key: string) => `${key} = :${key}`)\n    .join(\", \")\n}\n\nexport const buildAttributes = (body: any) => {\n  return Object.fromEntries(\n    Object.entries(body).map(([key, value]) => [\n      `:${key}`,\n      typeof value === \"string\" || typeof value === \"number\"\n        ? value\n        : JSON.stringify(value),\n    ])\n  )\n}\n\nexport const getItem = async (\n  params: GetItemParams\n): Promise<AWS.DynamoDB.DocumentClient.GetItemOutput> => {\n  const query = {\n    ...params,\n  }\n\n  return new Promise((resolve, reject) => {\n    dynamoDB.get(query, (err, result) => {\n      if (err) {\n        console.error(err)\n        reject(err)\n      } else {\n        resolve(result)\n      }\n    })\n  })\n}\n\nexport const scanItems = async (\n  params: ScanItemsParams\n): Promise<AWS.DynamoDB.DocumentClient.ScanOutput> => {\n  const query = {\n    ...params,\n  }\n\n  return new Promise((resolve, reject) => {\n    dynamoDB.scan(query, (err, result) => {\n      if (err) {\n        console.error(err)\n        reject(err)\n      } else {\n        resolve(result)\n      }\n    })\n  })\n}\n\nexport const deleteItem = async (\n  params: DeleteItemParams\n): Promise<AWS.DynamoDB.DocumentClient.DeleteItemOutput> => {\n  const query = {\n    ...params,\n  }\n\n  return new Promise((resolve, reject) => {\n    dynamoDB.delete(query, (err, result) => {\n      if (err) {\n        console.error(err)\n        reject(err)\n      } else {\n        resolve(result)\n      }\n    })\n  })\n}\n"
  },
  {
    "path": "examples/serverless-rest-example/src/manageItems.ts",
    "content": "import { APIGatewayEvent } from \"aws-lambda\"\nimport { APIResponse } from \"./types\"\nimport * as db from \"./dynamodb\"\nimport uuidv4 from \"uuid/v4\"\n\nfunction response(statusCode: number, body: any) {\n  return {\n    statusCode,\n    body: JSON.stringify(body),\n  }\n}\n\n// fetch using /item/ID\nexport const getItem = async (event: APIGatewayEvent): Promise<APIResponse> => {\n  const itemId = event.pathParameters ? event.pathParameters.itemId : \"\"\n\n  const item = await db.getItem({\n    TableName: process.env.ITEM_TABLE!,\n    Key: { itemId },\n  })\n\n  if (item.Item) {\n    return response(200, {\n      status: \"success\",\n      item: item.Item,\n    })\n  } else {\n    return response(404, {\n      status: \"error\",\n      error: \"Item not found\",\n    })\n  }\n}\n\n// upsert an item\n// /item or /item/ID\nexport const updateItem = async (\n  event: APIGatewayEvent\n): Promise<APIResponse> => {\n  let itemId = event.pathParameters ? event.pathParameters.itemId : uuidv4()\n\n  let createdAt = new Date().toISOString()\n\n  if (event.pathParameters && event.pathParameters.itemId) {\n    // find item if exists\n    const find = await db.getItem({\n      TableName: process.env.ITEM_TABLE!,\n      Key: { itemId },\n    })\n    if (find.Item) {\n      // save createdAt so we don't overwrite on update\n      createdAt = find.Item.createdAt\n    } else {\n      return response(404, {\n        status: \"error\",\n        error: `Item not found ${event.pathParameters.itemId}`,\n      })\n    }\n  }\n\n  if (!event.body) {\n    return response(400, {\n      status: \"error\",\n      error: \"Provide a JSON body\",\n    })\n  }\n\n  let body = JSON.parse(event.body)\n\n  if (body.itemId) {\n    // this will confuse DynamoDB, you can't update the key\n    delete body.itemId\n  }\n\n  const item = await db.updateItem({\n    TableName: process.env.ITEM_TABLE!,\n    Key: { itemId },\n    UpdateExpression: `SET ${db.buildExpression(\n      body\n    )}, createdAt = :createdAt, lastUpdatedAt = :lastUpdatedAt`,\n    ExpressionAttributeValues: {\n      ...db.buildAttributes(body),\n      \":createdAt\": createdAt,\n      \":lastUpdatedAt\": new Date().toISOString(),\n    },\n    ReturnValues: \"ALL_NEW\",\n  })\n\n  return response(200, {\n    status: \"success\",\n    item: item.Attributes,\n  })\n}\n\nexport const deleteItem = async (\n  event: APIGatewayEvent\n): Promise<APIResponse> => {\n  const itemId = event.pathParameters ? event.pathParameters.itemId : null\n\n  if (!itemId) {\n    return response(400, {\n      status: \"error\",\n      error: \"Provide an itemId\",\n    })\n  }\n\n  // DynamoDB handles deleting already deleted files, no error :)\n  const item = await db.deleteItem({\n    TableName: process.env.ITEM_TABLE!,\n    Key: { itemId },\n    ReturnValues: \"ALL_OLD\",\n  })\n\n  return response(200, {\n    status: \"success\",\n    itemWas: item.Attributes,\n  })\n}\n"
  },
  {
    "path": "examples/serverless-rest-example/src/types.d.ts",
    "content": "export interface APIResponse {\n  statusCode: number\n  body: string\n}\n"
  },
  {
    "path": "examples/serverless-rest-example/tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"ES2019\",\n        \"module\": \"commonjs\",\n        \"outDir\": \"./dist\",\n        \"strict\": true,\n        \"baseUrl\": \"./\",\n        \"typeRoots\": [\"node_modules/@types\"],\n        \"types\": [\"node\"],\n        \"esModuleInterop\": true,\n        \"inlineSourceMap\": true,\n        \"lib\": [\"ES2019\"]\n    }\n}"
  },
  {
    "path": "gatsby-browser.js",
    "content": "// https://serverlesshandbook.dev/?product_id=72rJA8s-O_ZK0H7YXUOQug%3D%3D&product_permalink=qdNn&sale_id=KOlpO90OcOkb7-lTy3I-Cg%3D%3D\n\n// This is from SRD\nexport const onClientEntry = () => {\n  const query = new URLSearchParams(window.location.search)\n\n  if (\n    [\"72Mdpm2jTgsapPjXuWpuXg==\", \"72rJA8s-O_ZK0H7YXUOQug==\"].includes(\n      query.get(\"product_id\")\n    ) &&\n    [\"qdNn\", \"NsUlA\"].includes(query.get(\"product_permalink\")) &&\n    query.has(\"sale_id\")\n  ) {\n    window.localStorage.setItem(\"unlock_handbook\", JSON.stringify(true))\n    window.localStorage.setItem(\"sale_id\", JSON.stringify(query.get(\"sale_id\")))\n  }\n}\n"
  },
  {
    "path": "gatsby-config.js",
    "content": "module.exports = {\n  siteMetadata: {\n    title: `Serverless Handbook for Frontend Engineers`,\n    description: `Dive into modern backend. Understand any backend.`,\n    author: `@swizec`,\n    siteUrl: `https://serverlesshandbook.dev/`,\n    courseFirstLesson: `/getting-started`,\n    convertkit: {\n      defaultFormId: \"2103715\",\n      claimFormId: \"2175932\",\n    },\n    hasAuthentication: true,\n  },\n  plugins: [\n    \"@swizec/gatsby-theme-course-platform\",\n    {\n      resolve: \"gatsby-plugin-manifest\",\n      options: {\n        name: \"Serverless Handbook for Frontend Engineers\",\n        short_name: \"Serverless Handbook for Frontend Engineers\",\n        description:\n          \"Learn everything you need to dive into modern backend. Understand any backend.\",\n        start_url: \"/\",\n        background_color: \"#fff\",\n        theme_color: \"#FF002B\",\n        display: \"standalone\",\n        icon: \"./static/icon.png\",\n      },\n    },\n    \"gatsby-plugin-remove-serviceworker\",\n  ],\n}\n"
  },
  {
    "path": "now.json",
    "content": "{\n    \"version\": 2,\n    \"scope\": \"swizec\",\n    \"name\": \"serverlesshandbook-staging\",\n    \"alias\": \"serverlesshandbook.dev\",\n    \"routes\": [{\n        \"src\": \"^/(.*).html\",\n        \"headers\": {\n            \"cache-control\": \"public,max-age=0,must-revalidate\"\n        },\n        \"dest\": \"$1.html\"\n    }],\n    \"builds\": [{\n        \"src\": \"package.json\",\n        \"use\": \"@now/static-build\",\n        \"config\": {\n            \"distDir\": \"public\"\n        }\n    }]\n}"
  },
  {
    "path": "package.json",
    "content": "{\n    \"private\": true,\n    \"name\": \"serverlesshandbook.dev\",\n    \"version\": \"1.0.0\",\n    \"main\": \"index.js\",\n    \"license\": \"MIT\",\n    \"scripts\": {\n        \"start\": \"gatsby develop\",\n        \"clean\": \"gatsby clean\",\n        \"build\": \"gatsby build\",\n        \"serve\": \"gatsby serve\",\n        \"icon\": \"npx repng src/components/icon.js -d static -f icon.png -w 512 -h 512\",\n        \"card\": \"npx repng src/components/twitter-card.js -d static -f card.png -w 1024 -h 512\"\n    },\n    \"dependencies\": {\n        \"@swizec/gatsby-theme-course-platform\": \"^2.3.0\",\n        \"@theme-ui/color\": \"^0.6.1\",\n        \"gatsby\": \"^3.2.1\",\n        \"react\": \"^17.0.1\",\n        \"react-dom\": \"^17.0.1\",\n        \"typeface-lato\": \"^1.1.13\",\n        \"typeface-neuton\": \"^1.1.13\",\n        \"typescript\": \"^4.2.4\",\n        \"typography-theme-stow-lake\": \"^0.16.19\"\n    },\n    \"engines\": {\n        \"node\": \"12.x || 14.x\"\n    }\n}\n"
  },
  {
    "path": "src/@swizec/gatsby-theme-course-platform/components/FormCK/formsQuery.js",
    "content": "export const formsQuery = `\n  query {\n    site {\n      siteMetadata {\n        convertkit {\n          defaultFormId\n        }\n      }\n    }\n  }\n`\n"
  },
  {
    "path": "src/@swizec/gatsby-theme-course-platform/components/FormCK/useFormsQuery.js",
    "content": "import { useStaticQuery, graphql } from \"gatsby\"\n\nexport const useFormsQuery = () => {\n  // change this query when you add forms\n  const { site } = useStaticQuery(graphql`\n    query {\n      site {\n        siteMetadata {\n          convertkit {\n            defaultFormId\n            claimFormId\n          }\n        }\n      }\n    }\n  `)\n\n  return site.siteMetadata.convertkit\n}\n"
  },
  {
    "path": "src/@swizec/gatsby-theme-course-platform/components/headerLogo.js",
    "content": "import React from \"react\"\nimport { NavLink } from \"theme-ui\"\n\nconst HeaderLogo = ({ siteTitle, logo }) => {\n  return (\n    <NavLink\n      variant=\"nav\"\n      href=\"/\"\n      sx={{\n        width: \"100%\",\n        maxWidth: 220,\n        alignItems: \"center\",\n        pl: [0, 2, 2],\n      }}\n    >\n      <p>{siteTitle}</p>\n    </NavLink>\n  )\n}\n\nexport default HeaderLogo\n"
  },
  {
    "path": "src/@swizec/gatsby-theme-course-platform/components/layout.js",
    "content": "import React, { useState, useRef } from \"react\"\nimport { Box, Flex } from \"theme-ui\"\nimport { Sidenav, Pagination } from \"@theme-ui/sidenav\"\nimport {\n  Footer,\n  Head,\n  Header,\n  Reactions,\n} from \"@swizec/gatsby-theme-course-platform\"\nimport Nav from \"./nav\"\nimport { Paywall, SnipContent, usePaywall } from \"../../../components/Paywall\"\n\nconst Sidebar = (props) => {\n  const { unlocked: contentUnlocked } = usePaywall(props.location.pathname)\n\n  return (\n    <Flex\n      sx={{\n        pt: 64,\n      }}\n    >\n      <Box\n        as={Sidenav}\n        ref={props.nav}\n        open={props.open}\n        onClick={(e) => {\n          props.setMenu(false)\n        }}\n        onBlur={(e) => {\n          props.setMenu(false)\n        }}\n        onFocus={(e) => {\n          props.setMenu(true)\n        }}\n        sx={{\n          width: [256, 256, 320],\n          flex: \"none\",\n          px: 3,\n          mt: [64, 0],\n          ul: {\n            p: 0,\n            m: 0,\n          },\n          \"ul > li\": {\n            mb: 0,\n          },\n          \"ul > li > a\": {\n            p: \"8px\",\n          },\n          maxHeight: \"100vh !important\",\n          overflowY: \"scroll\",\n          height: \"100%\",\n        }}\n      >\n        <Nav />\n      </Box>\n      <Box\n        sx={{\n          width: \"100%\",\n          minWidth: 0,\n          maxWidth: 768,\n          minHeight: \"calc(100vh - 64px)\",\n          mx: \"auto\",\n          px: [3, 4],\n          pb: 5,\n        }}\n      >\n        {contentUnlocked ? (\n          <main id=\"content\">{props.children}</main>\n        ) : (\n          <main id=\"content\">\n            <SnipContent>{props.children}</SnipContent>\n          </main>\n        )}\n        {contentUnlocked ? <Reactions page={props.href} /> : null}\n        <Paywall pagePath={props.location.pathname} />\n        <Nav\n          pathname={props.location.pathname}\n          components={{\n            wrapper: Pagination,\n          }}\n        />\n      </Box>\n    </Flex>\n  )\n}\n\nconst Layout = (props) => {\n  const fullwidth = props.location.pathname === \"/\"\n  const [menu, setMenu] = useState(false)\n  const nav = useRef(null)\n\n  if (props.pageContext.frontmatter) {\n    props = {\n      ...props,\n      ...props.pageContext.frontmatter,\n    }\n  }\n\n  return (\n    <Box\n      sx={{\n        variant: \"styles.root\",\n      }}\n    >\n      <Header\n        siteTitle=\"Serverless Handbook\"\n        courseFirstLesson={props.courseFirstLesson}\n        showMenu={false}\n        fullwidth={fullwidth}\n        menu={menu}\n        setMenu={setMenu}\n        nav={nav}\n      />\n      {fullwidth ? (\n        <>\n          <Head {...props} />\n          <main id=\"content\">{props.children}</main>\n        </>\n      ) : (\n        <Sidebar {...props} nav={nav} open={menu} setMenu={setMenu}>\n          <Head {...props} />\n          {props.children}\n        </Sidebar>\n      )}\n      <Footer />\n    </Box>\n  )\n}\n\nexport default Layout\n"
  },
  {
    "path": "src/@swizec/gatsby-theme-course-platform/components/nav.mdx",
    "content": "- [Getting Started](/getting-started)\n- [Serverless Pros & Cons](/serverless-pros-cons)\n- [AWS, Azure, Vercel, Netlify, or Firebase?](/serverless-flavors)\n- [Good serverless DX](/serverless-dx)\n- [Architecture principles](/serverless-architecture-principles)\n- [Lambdas, queues, etc](/serverless-elements)\n- [Robust backend design](/robust-backend-design)\n- [Where to store data](/databases)\n- [Creating a REST API](/serverless-rest-api)\n- [Using GraphQL](/serverless-graphql)\n- [Lambda pipelines](/lambda-pipelines)\n- [Monitoring serverless apps](/serverless-monitoring)\n- [Dev, QA, and prod](/dev-qa-prod)\n- [Serverless performance](/serverless-performance)\n- [Serverless Chrome puppeteer](/serverless-chrome-puppeteer)\n- [Handling secrets](/handling-secrets)\n- [Dealing with authentication](/serverless-authentication)\n- [Glossary](/glossary)\n- [Appendix: More databases](/appendix-more-databases)\n- [Download PDF/epub/mobi](/downloads)\n"
  },
  {
    "path": "src/@swizec/gatsby-theme-course-platform/constants/footerLinks.js",
    "content": "export default [\n    {\n        Link: 'https://github.com/Swizec/serverlesshandbook.dev',\n        Title: 'GitHub',\n    },\n    {\n        Link: 'https://serverlesshandbook.dev/',\n        Title: 'Serverless Handbook',\n    },\n] "
  },
  {
    "path": "src/components/ClaimForm.js",
    "content": "import React from \"react\"\nimport { useEmailForm } from \"@swizec/gatsby-theme-course-platform/src/components/FormCK\"\nimport BouncingLoader from \"@swizec/gatsby-theme-course-platform/src/components/BouncingLoader\"\nimport { Flex, Label, Input, Box, Button, Text, Heading, Image } from \"theme-ui\"\nimport emailRobot from \"@swizec/gatsby-theme-course-platform/src/images/email-robot.gif\"\nimport { useLocalStorage } from \"./useLocalStorage\"\n\nasync function createUser({ name, email }) {\n  await fetch(\n    \"https://tq43ps6oh2.execute-api.us-east-1.amazonaws.com/dev/gumroadPing\",\n    {\n      method: \"POST\",\n      mode: \"no-cors\",\n      body: new URLSearchParams({\n        product_permalink: \"https://gum.co/NsUlA\",\n        email,\n        full_name: name,\n      }).toString(),\n    }\n  )\n}\n\nexport const ClaimForm = () => {\n  const [unlockHandbook, setUnlockHandbook] = useLocalStorage(\"unlock_handbook\")\n  const [saleId, setSaleId] = useLocalStorage(\"sale_id\")\n\n  const {\n    formSuccess,\n    onSubmit,\n    uniqueId,\n    register,\n    formState,\n    submitError,\n  } = useEmailForm(\"claim\", async (formData) => {\n    await createUser(formData)\n    setUnlockHandbook(true)\n    setSaleId(\"came-from-a-claim\")\n    window.location.href = \"/thanks\"\n  })\n\n  if (formSuccess) {\n    return (\n      <Flex\n        sx={{\n          justifyContent: \"center\",\n          flexDirection: \"column\",\n        }}\n      >\n        <Heading>Thank you ❤️</Heading>\n        <p>\n          My robots have sent email that tells you how to finish setting up your\n          account. Redirecting you to the first chapter with a temporary unlock.\n        </p>\n        <p>Finish setting up your account and you can login from any device.</p>\n        <Image src={emailRobot} />\n      </Flex>\n    )\n  }\n\n  console.log(formState)\n\n  return (\n    <Flex\n      as=\"form\"\n      onSubmit={onSubmit}\n      sx={{\n        justifyContent: \"center\",\n        flexDirection: \"column\",\n        \"& .address\": { display: \"none\" },\n      }}\n    >\n      <Label htmlFor={`${uniqueId}-name`}>Your Name</Label>\n      <Input\n        id={`${uniqueId}-name`}\n        type=\"text\"\n        name=\"name\"\n        {...register(\"name\", {\n          required: \"⚠️ Name is required\",\n          message: \"⚠️ Name is required\",\n        })}\n        placeholder=\"Your name\"\n      />\n      {formState.errors.name && <span>{formState.errors.name.message}</span>}\n\n      <Label htmlFor={`${uniqueId}-email`} sx={{ mt: 2 }}>\n        Your Email\n      </Label>\n      <Input\n        id={`${uniqueId}-email`}\n        type=\"email\"\n        name=\"email\"\n        {...register(\"email\", {\n          required: \"⚠️ E-mail is required\",\n          pattern: {\n            value:\n              \"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$\",\n            message: \"⚠️ Invalid email address\",\n          },\n        })}\n        placeholder=\"Your email address\"\n      />\n      {formState.errors.email && <span>{formState.errors.email.message}</span>}\n\n      <Label htmlFor={`${uniqueId}-sharelink`} sx={{ mt: 2 }}>\n        Share link\n      </Label>\n      <Input\n        id={`${uniqueId}-sharelink`}\n        type=\"text\"\n        name=\"share_link\"\n        {...register(\"share_link\", {\n          required: \"⚠️ Share link is required\",\n          message: \"⚠️ Share link is required\",\n        })}\n        placeholder=\"Where did you share? I'd love to see your post\"\n      />\n      {formState.errors.share_link && (\n        <span>{formState.errors.share_link.message}</span>\n      )}\n\n      <Box className=\"address\">\n        <Label htmlFor={`${uniqueId}-address`} className=\"required\">\n          Your Address\n        </Label>\n        <input\n          className=\"required\"\n          autoComplete=\"nope\"\n          type=\"text\"\n          id={`${uniqueId}-address`}\n          name=\"address\"\n          {...register}\n          placeholder=\"Your address here\"\n        />\n      </Box>\n      <Button\n        type=\"submit\"\n        variant=\"buy-shiny\"\n        disabled={formState.isSubmitting}\n        sx={{ mt: 2 }}\n      >\n        {formState.isSubmitting ? <BouncingLoader /> : \"Claim my copy\"}\n      </Button>\n\n      {submitError && <p dangerouslySetInnerHTML={{ __html: submitError }}></p>}\n\n      <Text sx={{ fontSize: \"0.8em\", mb: 2, textAlign: \"center\" }}>\n        I like privacy too. No spam, no selling your data.\n      </Text>\n    </Flex>\n  )\n}\n"
  },
  {
    "path": "src/components/Paywall.js",
    "content": "import React, { useRef, useEffect } from \"react\"\nimport ReactDOMServer from \"react-dom/server\"\nimport { useAuth } from \"react-use-auth\"\nimport { Box } from \"theme-ui\"\nimport { wrapRootElement } from \"gatsby-plugin-theme-ui/gatsby-browser\"\n\nimport { default as PaywallCopy } from \"../components/paywall-copy\"\nimport QuickThanks from \"./quickthanks\"\nimport { useLocalStorage } from \"./useLocalStorage\"\n\nfunction toggleLockedContent(show) {\n  if (typeof window !== \"undefined\") {\n    let children = document.getElementById(\"content\").children\n\n    let isLocked = false\n    for (let child of children) {\n      if (child.id === \"lock\") isLocked = true\n      if (isLocked === true) {\n        child.style.display = show ? \"block\" : \"none\"\n      }\n    }\n  }\n}\n\nfunction hidePaywall(paywallDiv) {\n  if (paywallDiv.current) {\n    paywallDiv.current.style = `display: none`\n  }\n\n  const overlays =\n    typeof window !== \"undefined\" &&\n    document.querySelectorAll(\".fadeout-overlay\")\n\n  if (overlays) {\n    for (let overlay of overlays) {\n      overlay.style = \"display: none\"\n    }\n  }\n\n  // show content\n  toggleLockedContent(true)\n}\n\nfunction showPaywall(paywallDiv) {\n  // hide content\n  toggleLockedContent(false)\n\n  const overlay = typeof window !== \"undefined\" && document.createElement(\"div\")\n  const main =\n    typeof window !== \"undefined\" && document.querySelector(\"main#content\")\n\n  const style = `\n            background-image: linear-gradient(rgba(255, 255, 255, 0) 80%,  rgb(255, 255, 255, 1) 100%);\n            width: 100%;\n            top: 0px;\n            bottom: 0px;\n            position: absolute;\n          `\n  overlay.style = style\n  overlay.className = \"fadeout-overlay\"\n\n  main.style = \"position: relative;\"\n  main.appendChild(overlay)\n\n  const dimensions = main.getBoundingClientRect()\n\n  if (paywallDiv.current) {\n    paywallDiv.current.style = `\n            top: ${Math.round(dimensions.height * 0.1)}px;\n            width: ${Math.round(dimensions.width)}px;\n            background-color: var(--theme-ui-colors-muted,#f6f6ff);\n          `\n  }\n}\n\nconst LOCKED_PAGES = [\n  \"/getting-started\",\n  \"/serverless-pros-cons\",\n  \"/serverless-flavors\",\n  \"/serverless-dx\",\n  \"/serverless-architecture-principles\",\n  \"/serverless-elements\",\n  \"/robust-backend-design\",\n  \"/databases\",\n  \"/serverless-rest-api\",\n  \"/serverless-graphql\",\n  \"/lambda-pipelines\",\n  \"/serverless-monitoring\",\n  \"/dev-qa-prod\",\n  \"/serverless-performance\",\n  \"/serverless-chrome-puppeteer\",\n  \"/handling-secrets\",\n  \"/serverless-authentication\",\n  \"/glossary\",\n  \"/appendix-more-databases\",\n  \"/downloads\",\n]\n\nexport function usePaywall(pagePath) {\n  const paywallDiv = useRef(null)\n  const { isAuthorized } = useAuth()\n  const [unlockHandbook] = useLocalStorage(\"unlock_handbook\")\n  const [saleId] = useLocalStorage(\"sale_id\")\n  const [unlockedPages, setUnlockedPages] = useLocalStorage(\n    \"unlocked_pages\",\n    []\n  )\n  const hasLock = LOCKED_PAGES.includes(pagePath.replace(/\\/$/, \"\"))\n\n  const unlocked =\n    !hasLock ||\n    (pagePath &&\n      unlockedPages.filter((p) => p !== \"/downloads\").includes(pagePath)) ||\n    isAuthorized([\"ServerlessHandbook\"]) ||\n    (unlockHandbook && saleId)\n\n  useEffect(() => {\n    // doesn't run during on-page navigation for some reason\n    if (typeof window !== \"undefined\") {\n      window.requestAnimationFrame(() => {\n        if (unlocked) {\n          hidePaywall(paywallDiv)\n        } else {\n          showPaywall(paywallDiv)\n        }\n      })\n    }\n  }, [unlocked])\n\n  function unlockCurrentPage() {\n    setUnlockedPages((unlockedPages) => [...unlockedPages, pagePath])\n  }\n\n  return { unlocked, paywallDiv, unlockCurrentPage, SnipContent }\n}\n\nexport function SnipContent({ children }) {\n  const html = ReactDOMServer.renderToString(children).split(\n    '<div id=\"lock\"></div>'\n  )[0]\n\n  //   return wrapRootElement({\n  //     element: children,\n  //   })\n\n  //   return wrapRootElement({\n  //     element: <div dangerouslySetInnerHTML={{ __html: html }} />,\n  //   })\n\n  return <div dangerouslySetInnerHTML={{ __html: html }} />\n\n  //   return (\n  //     <WrapRootElement\n  //       element={<div dangerouslySetInnerHTML={{ __html: html }} />}\n  //     />\n  //   )\n}\n\nexport const Paywall = ({ pagePath }) => {\n  const { unlocked, paywallDiv, unlockCurrentPage } = usePaywall(pagePath)\n\n  if (unlocked) {\n    return <QuickThanks />\n  } else {\n    return (\n      <Box id=\"paywall-copy\" ref={paywallDiv}>\n        <PaywallCopy unlockCurrentPage={unlockCurrentPage} />\n      </Box>\n    )\n  }\n}\n"
  },
  {
    "path": "src/components/TestCloudFunctions.js",
    "content": "import React, { useState } from \"react\"\nimport { Box, Button, Heading, Input, Label } from \"theme-ui\"\nimport prettier from \"prettier/standalone\"\nimport parserBabel from \"prettier/parser-babel\"\n\nexport const TestCloudFunction = ({\n  serviceName,\n  urlPlaceholder,\n  jsonPlaceholder,\n  defaults = false,\n}) => {\n  const [url, setUrl] = useState(defaults ? urlPlaceholder : \"\")\n  const [payload, setPayload] = useState(defaults ? jsonPlaceholder : \"\")\n  const [result, setResult] = useState(\"\")\n  const [error, setError] = useState(null)\n\n  async function runTest() {\n    setError(null)\n    setResult(null)\n\n    if (!url) {\n      setError(`Paste the URL you got from ${serviceName}`)\n    } else {\n      const corsUrl = `https://cors.bridged.cc/${url}`\n\n      try {\n        let res\n        if (payload) {\n          res = await fetch(corsUrl, {\n            method: \"POST\",\n            body: payload,\n            headers: {\n              \"Content-Type\": \"application/json\",\n            },\n          })\n        } else {\n          res = await fetch(corsUrl)\n        }\n\n        setResult(await res.text())\n      } catch (e) {\n        setError(e.message)\n      }\n    }\n  }\n\n  return (\n    <Box bg=\"muted\" p={[2, 3, 3]}>\n      <Heading as=\"h3\" mb={[1, 2, 2]}>\n        Try your function\n      </Heading>\n      <p>Leave payload empty for GET requests, valid JSON for POST.</p>\n      <Label>Target URL:</Label>\n      <Input\n        value={url}\n        onChange={(ev) => setUrl(ev.target.value)}\n        placeholder={urlPlaceholder}\n      />\n      <Label>JSON payload:</Label>\n      <Input\n        value={payload}\n        onChange={(ev) => setPayload(ev.target.value)}\n        placeholder={jsonPlaceholder}\n      />\n      <Button\n        onClick={runTest}\n        sx={{ mt: [1, 2, 2], mb: [2, 3, 3], cursor: \"pointer\" }}\n      >\n        Run {payload ? \"POST\" : \"GET\"} request\n      </Button>\n      {result ? (\n        <>\n          <Heading as=\"h4\">Result:</Heading>\n          <pre>\n            {prettier.format(result, {\n              parser: \"json\",\n              plugins: [parserBabel],\n            })}\n          </pre>\n        </>\n      ) : null}\n      {error ? (\n        <>\n          <Heading color=\"red\" as=\"h4\">\n            Error:\n          </Heading>\n          <pre style={{ color: \"red\" }}>{error}</pre>\n        </>\n      ) : null}\n    </Box>\n  )\n}\n"
  },
  {
    "path": "src/components/homepage.js",
    "content": "import React from \"react\"\nimport { useAuth } from \"react-use-auth\"\nimport { Heading, Flex, Box, Text, Button } from \"theme-ui\"\nimport { GumroadButton, TinyFormCK } from \"@swizec/gatsby-theme-course-platform\"\nimport { StaticImage } from \"gatsby-plugin-image\"\n\nexport const ChapterHeading = ({ sx }) => {\n  const { isAuthorized } = useAuth()\n\n  if (isAuthorized([\"ServerlessHandbook\"])) {\n    return <Heading sx={sx}>Chapters</Heading>\n  } else {\n    return <Heading sx={sx}>Chapter Previews</Heading>\n  }\n}\n\nexport const NavGrid = (props) => (\n  <Box\n    {...props}\n    sx={{\n      fontFamily: \"heading\",\n      ul: {\n        listStyle: \"none\",\n        p: 0,\n        display: \"grid\",\n        gridGap: 3,\n        gridTemplateRows: [`repeat(9, 1fr)`, `repeat(5, 1fr)`],\n        gridTemplateColumns: [\"repeat(2, 1fr)\", \"repeat(3, 1fr)\"],\n        gridAutoFlow: [\"dense\", \"column\"],\n        counterReset: \"nav-grid\",\n      },\n      li: {\n        fontWeight: \"bold\",\n        fontSize: [1, 2, 2],\n        counterIncrement: \"nav-grid\",\n        mb: 0,\n        \"::before\": {\n          content: \"counter(nav-grid)\",\n          display: \"inline-block\",\n          pr: 1,\n        },\n      },\n      a: {\n        color: \"inherit\",\n        textDecoration: \"none\",\n        transition: \"color .2s ease-out\",\n        \":hover,:focus\": {\n          color: \"primary\",\n        },\n      },\n    }}\n  />\n)\n\nconst CoverImage = () => {\n  return (\n    <a href=\"https://geni.us/serverless-handbook\">\n      <StaticImage\n        src=\"../images/cover.png\"\n        alt=\"Serverless Handbook cover\"\n        loading=\"eager\"\n        objectFit=\"cover\"\n        objectPosition=\"50% 50%\"\n      />\n    </a>\n  )\n}\n\nexport const HomeTitle = () => (\n  <Flex sx={{ flexWrap: \"wrap\" }}>\n    <Box\n      sx={{\n        p: 3,\n        minWidth: 250,\n        flex: 1,\n        textAlign: \"center\",\n        margin: \"auto auto\",\n      }}\n    >\n      <Heading sx={{ fontSize: 6 }}>\n        Serverless Handbook\n        <Text sx={{ fontSize: 4, display: \"block\" }}>\n          for frontend engineers\n        </Text>\n      </Heading>\n      <Text>Dive into modern backend. Understand any backend</Text>\n\n      <Box sx={{ my: 3 }}>\n        <Flex\n          sx={{\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            mb: 1,\n            flexDirection: [\"column\", \"row\"],\n          }}\n        >\n          <Button\n            variant=\"buy-shiny\"\n            as=\"a\"\n            sx={{ mr: [0, 2], mb: [1, 0] }}\n            href=\"https://geni.us/serverless-handbook\"\n          >\n            Buy now on Amazon\n          </Button>\n          <a\n            className=\"gumroad-button\"\n            href=\"https://gum.co/NsUlA\"\n            data-gumroad-single-product=\"true\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            Buy Digital for $15\n          </a>\n        </Flex>\n        <Text sx={{ fontSize: 0, display: \"block\" }}>\n          #1 new release in Web Development\n        </Text>\n      </Box>\n\n      <Box\n        sx={{\n          p: 3,\n          minWidth: 250,\n          flex: 1,\n          textAlign: \"center\",\n          display: [\"block\", \"none\"],\n        }}\n      >\n        <CoverImage />\n      </Box>\n\n      <Box sx={{ mt: 3 }}>\n        <TinyFormCK copyBefore=\"\" submitText=\"Send it to me! 💌\">\n          <Heading sx={{ fontSize: 4, pt: 2 }}>Get your free chapter!</Heading>\n          <p>\n            Wanna see what’s in Serverless Handbook, but not ready to buy the\n            full book? Start with a free chapter.\n          </p>\n        </TinyFormCK>\n      </Box>\n    </Box>\n\n    <Box\n      sx={{\n        p: 3,\n        minWidth: 250,\n        flex: 1,\n        textAlign: \"center\",\n        display: [\"none\", \"block\"],\n      }}\n    >\n      <CoverImage />\n    </Box>\n  </Flex>\n)\n"
  },
  {
    "path": "src/components/logo.js",
    "content": "import React from \"react\"\nimport styled from \"@emotion/styled\"\nimport { layout } from \"styled-system\"\n\nconst Svg = styled(({ width, height, ...props }) => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" {...props} />\n))`\n  transform: rotate3d(1, 1, 1, 0deg);\n  ${layout}\n`\n\n\nconst Logo = props => {\n\n  return (\n    <Svg\n      viewBox=\"0 0 64 64\"\n      style={{\n        display: \"block\",\n        maxWidth: \"100%\",\n        margin: 0,\n        fill: \"none\",\n        stroke: \"cyan\",\n      }}\n      vectorEffect=\"non-scaling-stroke\"\n      width={props.size}\n      height={props.size}\n    ></Svg>\n  )\n}\n\nLogo.defaultProps = {\n  initial: false,\n  color: \"white\",\n  bg: \"transparent\",\n  strokeWidth: 2,\n  size: 256,\n}\n\nexport default Logo"
  },
  {
    "path": "src/components/paywall-copy.mdx",
    "content": "import { Box, Flex, Button } from \"theme-ui\"\nimport {\n  GumroadOverlay,\n  GumroadButton,\n  TinyFormCK,\n} from \"@swizec/gatsby-theme-course-platform\"\n\n<Box sx={{\n          mb: 4,\n          border: t => `1px solid ${t.colors.muted}`,\n          borderRadius: 2,\n          p: 3\n        }}>\n\nHello! 👋\n\nAre you a frontend engineer diving into backend? Do you have just that one bit of code that can't run in the browser? Something that deals with secrets and APIs?\n\nhttps://youtu.be/udqyBqCgLrU\n\nThat's what cloud functions are for my friend. You take a JavaScript function, run it on serverless, get a URL, and voila.\n\nBut that's easy mode. Any tutorial can teach you that.\n\n**What happens when you wanna build a real backend?** When you want to understand what's going on? Have opinions on REST vs GraphQL, NoSQL vs. SQL, databases, queues, talk about performance, cost, data processing, deployment strategies, developer experience?\n\n🤯\n\n### Unlock your free chapter!\n\nAccess to this chapter immediately, extra free chapter and Serverless crash course in your email ✌️\n\n<TinyFormCK\n  copyBefore=\"\"\n  submitText=\"Send crash course! 💌\"\n  onSuccess={() => setTimeout(props.unlockCurrentPage, 2000)}\n/>\n<br />\n\n[![](../images/buy-now-amazon.png)](https://geni.us/serverless-handbook)\n\n## Dive into modern backend. Understand any backend\n\n<p style={{ fontSize: \"1.1em\" }}>\n  <strong>Serverless Handbook</strong> shows you how with{\" \"}\n  <strong>360 pages</strong> for people like you getting into backend\n  programming.\n</p>\n\nWith **digital + paperback content** Serverless Handbook has been more than 1 year in development. Lessons learned from 14 years of building production grade websites and webapps.\n\n> With Serverless Handbook, Swiz teaches the truths of distributed systems – things will fail – but he also gives you insight on how to architect projects using reliability and resilience perspectives so you can monitor and recover.\n\n> ~ Thai Wood, author of Resilience Roundup\n\nIf you want to understand backends, grok serverless, or just get a feel for modern backend development, this is the book for you.\n\nServerless Handbook full of **color illustrations**, **code you can try**, and **insights you can learn**. But it's not a cookbook and it's not a tutorial.\n\n[![Serverless Handbook on your bookshelf](../images/serverless-handbook-bookshelf.jpg)](https://geni.us/serverless-handbook)\n\nYes, there's a couple tutorials to get you started, to show you how it fits together, but the focus is on high-level concepts.\n\n<p style={{ fontSize: \"1.1em\" }}>\n  <strong>Ideas</strong>, <strong>tactics</strong>, and{\" \"}\n  <strong>mindsets</strong> that you need. Because every project is different.\n</p>\n\nThe Serverless Handbook takes you _from your very first cloud function to modern backend mastery_. In the words of an early reader:\n\n> Serverless Handbook taught me high-leveled topics. I don't like recipe courses and these chapters helped me to feel like I'm not a total noob anymore.\n\n> The hand-drawn diagrams and high-leveled descriptions gave me the feeling that I don't have any critical \"knowledge gaps\" anymore.\n\n> ~ Marek C, engineer\n\nIf you can JavaScript, you can backend.\n\nPlus it looks great on your bookshelf 😉\n\n[![](../images/buy-now-amazon.png)](https://geni.us/serverless-handbook)\n\nCheers,<br/>\n~Swizec\n\n</Box>\n"
  },
  {
    "path": "src/components/quickthanks.mdx",
    "content": "import { Box } from \"theme-ui\"\n\n<Box sx={{\n          mb: 4,\n          border: t => `1px solid ${t.colors.muted}`,\n          borderRadius: 2,\n          variant: \"styles.pre\"\n        }}>\n\nThanks for supporting Serverless Handbook!\nConsider sharing it with friends ❤️\n\nAdd it to your bookshelf if you haven't.\n\n[![](../images/buy-now-amazon.png)](https://geni.us/serverless-handbook)\n\n[![Serverless Handbook on your bookshelf](../images/serverless-handbook-bookshelf.jpg)](https://geni.us/serverless-handbook)\n\nCheers,<br/>\n~Swizec\n\n</Box>\n"
  },
  {
    "path": "src/components/useLocalStorage.js",
    "content": "import { useState } from \"react\"\n\n// copypasta dependency borrowed from https://medium.com/javascript-in-plain-english/uselocalstorage-react-hook-2532e922d5b1\nexport function useLocalStorage(key, initialValue) {\n  // State to store our value\n  // Pass initial state function to useState so logic is only executed once\n  const [storedValue, setStoredValue] = useState(() => {\n    try {\n      // Get from local storage by key\n      const item =\n        typeof window !== \"undefined\" && window.localStorage.getItem(key)\n      // Parse stored json or if none return initialValue\n      return item ? JSON.parse(item) : initialValue\n    } catch (error) {\n      // If error also return initialValue\n      console.log(error)\n      return initialValue\n    }\n  })\n\n  // Return a wrapped version of useState's setter function that ...\n  // ... persists the new value to localStorage.\n  const setValue = (value) => {\n    try {\n      // Allow value to be a function so we have same API as useState\n      const valueToStore =\n        value instanceof Function ? value(storedValue) : value\n      // Save state\n      setStoredValue(valueToStore)\n      // Save to local storage\n      if (typeof window !== \"undefined\") {\n        window.localStorage.setItem(key, JSON.stringify(valueToStore))\n      }\n    } catch (error) {\n      console.log(error)\n    }\n  }\n\n  return [storedValue, setValue]\n}\n"
  },
  {
    "path": "src/gatsby-plugin-theme-ui/index.js",
    "content": "import { future } from \"@theme-ui/presets\"\nimport merge from \"lodash.merge\"\nimport { toTheme } from \"@theme-ui/typography\"\nimport typography from \"typography-theme-stow-lake\"\nimport amazonBuy from \"../images/buy-now-amazon.png\"\nimport \"typeface-neuton\"\nimport \"typeface-lato\"\n\nimport { courseTheme } from \"@swizec/gatsby-theme-course-platform\"\n\nconst theme = merge(\n  future,\n  {\n    buttons: {\n      buy: {\n        cursor: \"pointer\",\n        fontWeight: \"heading\",\n        background: `url(${amazonBuy})`,\n        backgroundSize: \"fit\",\n        backgroundPosition: \"center center\",\n        backgroundRepeat: \"no-repeat\",\n        textIndent: \"-1000%\",\n        width: 240,\n      },\n      \"buy-shiny\": {\n        cursor: \"pointer\",\n        fontWeight: \"heading\",\n        bg: \"highlight\",\n        borderRadius: 35,\n        \"&:hover\": { bg: \"secondary\" },\n      },\n    },\n  },\n  toTheme(typography),\n  courseTheme\n)\n\nexport default theme\n"
  },
  {
    "path": "src/pages/404.mdx",
    "content": "export const title = \"404\"\n\n# 404\n\nPage not found\n"
  },
  {
    "path": "src/pages/appendix-more-databases/index.mdx",
    "content": "---\ntitle: \"Appendix: Databases in more detail\"\ndescription: \"Serverless systems don't have a hard drive ... where do you keep your data?\"\nimage: \"./img/databases.png\"\n---\n\n# Appendix: Databases in more detail\n\nRead additional details and insights that didn't fit into the main chapter on databases.\n\n## Flat file database\n\n![](../../images/flat-file.png)\n\nThe simplest way to store data is a [flat file database](https://en.wikipedia.org/wiki/Flat-file_database). Even if you call it \"just organized files\".\n\nServerless systems don't have drives to store files so these flat file databases aren't a popular choice. You'd have to use S3 or similar, which negates some of the built-in advantages.\n\nWe mention them here because they're often a great choice and most engineers forget they exist.\n\n### Advantages of flat files\n\nCompared to other databases, flat files have zero overhead. Your data goes straight to storage without your database adding any logic on top.\n\nThis gives flat files amazing write performance. As long as you're adding data to the end of a file.\n\nOptimizing file access for speed comes down to your operating system. Performance mostly has to do with [memory paging](https://en.wikipedia.org/wiki/Paging), [filesystem](https://en.wikipedia.org/wiki/File_system) configuration, [direct memory access](https://en.wikipedia.org/wiki/Direct_memory_access), and hardware-level optimizations.\n\nThe end result is:\n\n- **fast append performance**, because you stream data from memory to drive without processor involvement\n- **fast read performance for common reads**, because computers use their memory as read-through cache. Once you read a file, subsequent reads come from much faster memory\n- **fast search for predicted lookups**, because you can structure your files in a way that makes common lookups instant\n- **great scalability**, because you can spread your data over as many servers as you'd like\n\n### Disadvantages of flat files\n\nWhere flat files struggle are data updates. \n\nTo add a line at the beginning of a file, you have to move the whole thing. To change a line in the middle, you have to update everything that comes after.\n\nAnother problem is lack of a query interface. \n\nYou have to read all your files to compare, analyze, and search. If you didn't think of a use-case beforehand, you're left with a slow search through everything.\n\n- **slow data updates**, because you often have to rewrite more than just the changes\n- **no data shape guarantees**, because you can store individual  data items any way you like. If you change your schema, you have to deal with inconsistencies, or rewrite your data\n- **slow broad reads**, because you lose benefits of read-through-cache, if you read data across your entire database with random patterns\n- **no ACID compliance** unless you build it yourself at the application layer\n\n<div id=\"lock\" />\n\n### How to use flat files\n\nA flat-file database happens any time you write data to files in a structured manner. Photos on your phone are a great example. \n\nWhen you take a photo, your phone stores it as a file. Filenames often follow a naming convention – `IMG_0001`, `IMG_0002`, etc. Some cameras add dates.\n\nNow you can sequentially access photos. Start at the beginning, go until the end. You can even keep track of how far you've gotten so you can jump ahead next time.\n\nWhat about finding all photos from a certain date?\n\nThat's why images contain [EXIF metadata](https://en.wikipedia.org/wiki/Exif) – information about date, time, GPS location, even phone orientation. It's all stored in the file in a structured way.\n\nTo find images from a certain date, you can search through your files and look at the metadata they contain. \n\n#### Hierarchical and flat file organization\n\nYou can speed up seek operations by adding common search keys to your file names.\n\nA good example is organizing photos by year - month - date. Each year is a directory containing directories for months. Each of those contains directories for days. Those contain photos.\n\nHierarchical storage slashes search speed by a huge factor. You can jump right into a specific category (date).\n\nBut hierarchical storage makes scanning harder. Counting all images now requires a recursive search through directories.\n\nA better approach might be encoding your meta data in the filename. Something like `IMG_${year}-${month}-${day}-001.jpg`.\n\nGives you quick access to specific dates *and* fast scans across many. 👌\n\n### When you should store data in flat files\n\nDue to its low overhead, flat file storage is a great choice when you're looking for speed and simplicity.\n\n**Use flat files when:**\n\n1. You need fast append-only writes\n2. You have simple querying requirements\n3. You read data more often than you write data\n4. You write data that you rarely read\n\n**Avoid flat files when:**\n\n1. You need to cross-reference data or use complex queries\n2. You need fast access across your entire database\n3. Your data often changes\n\nNo. 3 is the flat file database killer.\n\nThe most common use cases for flat files are logs, large datasets, and large binary files (image, video, etc).\n\nYou often append logs and rarely read them. You store large datasets as structured files for easy sharing. You save images and rarely update, and they contain orders of magnitude more binary data than structured metadata.\n\n## Relational databases – RDBMS\n\n![](../../images/relational_generic.png)\n\n[Relational databases](https://en.wikipedia.org/wiki/Relational_database) are the most common type of database people think of when you say \"database\". Data lives in a structured data model and many features exist to optimize performance.\n\nChoosing a relational database for your business data is almost always the right decision. \n\n### Advantages of relational databases\n\nRelational databases have been around since the 1970's. They're battle tested, reliable, and can adapt to almost any workload.\n\nSome modern solutions incorporate features from the NoSQL universe to become even more versatile. [Postgres](https://en.wikipedia.org/wiki/PostgreSQL) is a great example since it often outperforms NoSQL solutions on performance benchmarks.\n\nThe defining feature of relational databases is their relational data model. The relational data model makes it easy to model complex data using small isolated concepts. Everything else stems from there.\n\n**Decades of research have gone into optimizing relational database performance**. Down to features like reserving empty space on a hard drive so data commonly used together sits together to make access faster 🤯\n\nWith a relational database, you get:\n\n- **fast write performance**, because most systems write to memory first and save to drive later\n- **fast read performance**, because you can build lookup data structures [(indexes)](https://en.wikipedia.org/wiki/Database_index) for common queries\n- **flexible querying**, because you can use a query language (usually SQL) to access any data in any configuration\n- **strict ACID compliance**, because it's a core design objective\n- some **logical data validation**, because you can describe the shape of your data and have the database enforce its consistency\n\n### Disadvantages of relational databases\n\nBenefits of relational databases come with the downside of being harder to use, requiring more expertise to tune performance, and some loss in flexibility.\n\nYou can create a database that's fast as lightning, or shoot yourself in the foot.\n\nMain disadvantages are:\n\n- **high level of complexity**, because it's easy to get started and the fine-tuning rabbit hole goes forever deep\n- **performance traps at scale**, because a database that's blazing fast at 10,000 entries, might crawl to a halt at 10,000,000 entries\n- **tradeoffs between read and write performance**, because you can get more read performance by sacrificing write performance and vice-versa. Mostly to do with index building\n- **data shape inflexibility**, because with a typical configuration, you have to redefine the shape of your data every time you add a property\n- **lack of horizontal scalability**, because relational databases can't give you most of their benefits when split across many servers\n\nA common solution for flexibility is to add a blobby JSON field to every model. You lose automatic data integrity and performance optimization on that field, but gain the flexibility to store any data in any shape. Perfect use-case for less important metadata.\n\n### How to use a relational DB\n\nWe'll focus on the basics here. You should approach the rest with just-in-time learning – run into a problem, learn how to solve it. ✌️\n\nYou start with a database server.\n\n#### A database server\n\nSince you don't want to run your own servers (the whole point of serverless), you'll need a provider. 3rd party services are okay, if your serverless provider doesn't offer their own.\n\nUsing a \"native\" service cuts down on network overhead and makes your system faster. With some luck, your provider runs the database and cloud functions in the same datacenter. Perhaps even on the same physical machine. 🤘\n\nIn the AWS world, I've found [RDS – Relational Database Service](https://aws.amazon.com/rds/) works great. Gives you a stable provisioned database server.\n\nDespite giving you a reserved database instance, RDS still offers advantages over rolling your own:\n\n- drive space grows as necessary\n- regular updates run automatically\n- if a server falls over, RDS brings up a new one with the same data\n- automatic backups\n- easy restore from backup\n- optional multi-zone replication for extra reliability (copies your DB over several data centers)\n\nA more serverless version of RDS is [Amazon Aurora DB](https://aws.amazon.com/rds/aurora/). Implemented as a layer on top of RDS, it scales your database based on usage. Even shutting down when nothing's happening. Less expensive for intermittent workloads *and* you don't have to predict how much power you'll need.\n\nWhen choosing which relational database software to use, **always choose Postgres**. It's open source, crazy fast, and with great support for modern NoSQL features.\n\nhttps://twitter.com/Swizec/status/1210371195889049600\n\n#### Model your data\n\nThe next step is to model your data.\n\n*How* to model your data is as much an art as it is a science. Sort of a mix between [domain modeling](https://en.wikipedia.org/wiki/Domain_model) and [object modeling](https://en.wikipedia.org/wiki/Object_model).\n\nModels also known as tables store each property as a column. Model instances live as rows inside those tables.\n\nLet's say you're building a blog. You have `authors` and `posts`.\n\nEach author has an `id` (automatically assigned), a `created_at` timestamp, and a `name`. Each post also has an `id` (automatically assigned), a `created_at` timestamp, a `title`, and some `content`.\n\nIds are numbers, timestamps are timestamps, the rest is text. A relational database ensures that's always true so you can expect valid data.\n\nTo create a connection between models, you use a [foreign key](https://en.wikipedia.org/wiki/Foreign_key). A column that points to the identifier of a different table.\n\nFor `authors` and `posts` you get a schema like this:\n\n![](../../images/relational.png)\n\nWhich in SQL – [Standard Query Language](https://en.wikipedia.org/wiki/SQL) – looks something like this:\n\n```sql\nCREATE TABLE IF NOT EXISTS \"authors\" (\n\t\"id\" serial,\n\t\"created_at\" timestamp,\n\t\"name\" text,\n\tPRIMARY KEY( id )\n);\n\nCREATE TABLE IF NOT EXISTS \"posts\" (\n\t\"id\" serial,\n\t\"created_at\" timestamp,\n\t\"title\" text,\n\t\"content\" text,\n\t\"author_id\" integer,\n\tPRIMARY KEY( id, author_id ),\n\tFOREIGN KEY ( author_id ) REFERENCES authors( id )\n);\n```\n\nThat creates 2 empty tables in your database and connects them via a foreign key. Postgres automatically increments identifiers and ensures uniqueness as you insert new rows.\n\nHaving the `posts` table \"belong to\" (point at) the `authors` table means each author can have multiple posts.\n\n#### Query your data\n\nYou interact with a relational database primarily through [SQL](https://en.wikipedia.org/wiki/SQL). Many web server frameworks come with an [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) – object-relational-mapping – layer on top of SQL that simplifies common operations.\n\nRegardless of using an ORM, you'll have to know SQL for anything complex. At least have an understanding of how it works.\n\nA basic query that fetches all `authors` looks like this:\n\n```sql\nSELECT * FROM authors;\n```\n\nWriting keywords in all caps is customary but not required. I think it stems from ye olden times before syntax highlighting.\n\nTo fetch just names, you'd do this:\n\n```sql\nSELECT name FROM authors;\n```\n\nSelect *what* from *where*. SQL is meant to be readable as natural English. Used to be the main user interface after all.\n\nTo fetch authors created after a certain date, something like this:\n\n```sql\nSELECT name FROM authors WHERE created_at >= '2019-12-26';\n```\n\nYou can put almost any condition in a `WHERE` clause.\n\nWhere life gets real tricky real fast is selecting data from multiple tables. You have to use [SQL joins](https://en.wikipedia.org/wiki/Join_(SQL)), which are based on set arithmetic. \n\nIf you want a list of post titles and dates with each author:\n\n```sql\nSELECT a.name, p.title, p.created_at\nFROM authors a, posts po\nWHERE p.author_id = a.id;\n```\n\nThis is called an [inner join](https://en.wikipedia.org/wiki/Join_(SQL)#Inner_join) where you take a cartesian join combining every row in `authors` with every row in `posts` and filter away the non-matches.\n\nThose are some basics that cover most use-cases. It takes some practice to use SQL effectively so practice away :)\n\n#### Speed up your data\n\nRelational databases use a [query planner](https://en.wikipedia.org/wiki/Query_plan) to execute SQL queries as efficiently as possible. You often don't even have to think about performance.\n\nCommon ways to speed up your database include:\n\n1. **Adding indexes** – data structures that help your database find data matching specific queries. Depending on the type of index you choose, it can behave like a directory tree, or like a hash table\n2. **Denormalization** – storing properties often used together in the same table even if it means duplicating some data. Like having an author name field in each post.\n3. **Partitioning** – telling your database how to chunk a large table into files so common lookups read from the same physical file\n\nTuning a relational database for performance is a whole field of software engineering so I wouldn't worry about it too much. Learn about it when you need to ✌️\n\n### When you should store data in a relational DB\n\nChoosing a relational database is almost always the correct choice.\n\n**Use relational DBs when:**\n\n1. You don't know how you're using your data\n2. You benefit from data integrity\n3. You need good performance up to hundreds of million entries\n4. Your app fits in a single data center (availability zone)\n5. You often use different objects together\n\n**Avoid relational DBs when:**\n\n1. You're storing lots of binary data (images, video)\n2. You don't care about data integrity\n3. You don't want to invest in initial setup\n4. You just need a quick way to save some data\n5. You have so much data it doesn't fit on 1 server\n\nThis makes relational databases the perfect choice for most applications. You wouldn't use them to store files, but should consider it for metadata about those files. They're also not a great choice for fast append-only writes like logs or tweets.\n\nI wouldn't worry about number 5. If you ever reach the scale where your data doesn't fit in a single database, you'll have a team to solve the problem for you :)\n\n## The NoSQL approach\n\n![](../../images/nosql.png)\n\n\"NoSQL\" is a broad range of technologies built for different purposes with different design goals in mind. A sort of catch-all for any database that doesn't use relational data models.\n\nYes even flat file storage is a sort of NoSQL database.\n\nWikipedia offers [a great description of NoSQL databases](https://en.wikipedia.org/wiki/NoSQL)\n\n> The data structures used by NoSQL databases are different from those used by default in relational databases, making some operations faster in NoSQL. The particular suitability of a given NoSQL database depends on the problem it must solve.\n\nThis variety is where NoSQL differs most from relational databases. Where relational databases aim to fit most use cases, NoSQL often aims to solve a specific problem.\n\n### Flavors of NoSQL\n\nYou can classify NoSQL databases in 4 broad categories:\n\n1. **key:value store** that works like a dictionary. A unique key points to a stored value. Offers fast read/write performance and is often used as a caching layer in front of a relational database. Some solutions offer sortable keys so you can perform data scans.\n2. **document store** that maps unique keys to documents. Basically key:value stores that allow complex values. They often support a query language that lets you search based on value contents, not just the key.\n3. **graph database** that stores graph data structures efficiently. Useful for data models with a lot of circular references like social graphs or road maps.\n4. **wide column database** that is a sort of mix between a document store and a relational database. Keys map to objects that all fit a schema, but that schema isn't prescriptive. There's no guarantee every object has every column, but you always know every column that might exist.\n\nEverything else is a variation on these themes. Most modern databases support multiple models.\n\nYou can stringify JSON objects into a key:value store to create a document store without querying support. Or you can store simple values in a document DB to create a key:value store with too much overhead.\n\nThere's nothing stopping you from forcing a graph to live inside a table database either. 😉\n\n### Which NoSQL flavor should you pick?\n\nIt really depends. What are you trying to do?\n\nI would prioritize a hosted fully managed database solution that my serverless provider offers. This cuts down on networking overhead, just like the RDBMS section, and makes your life easier because there's one less thing to manage.\n\nThen I would pick whatever fits my use case.\n\n**Use key:value stores** when you need blazing fast data with low overhead. Great for implementing caching layers and queue systems. [Redis](https://en.wikipedia.org/wiki/Redis) is a great choice here. [Memcached](https://en.wikipedia.org/wiki/Memcached), if you just need cache.\n\nI often use Redis *and* Memcached in real world projects.\n\n**Use a document DB or wide column store** when you want a generic database for business data that isn't a relational database. You get the benefit of flexibility and horizontal scalability. No schemas to prepare in advance and little thought about optimizing queries. \n\nYou pay for that 3 years down the line with inconsistent data. Learned my lesson ✌️\n\n[MongoDB](https://en.wikipedia.org/wiki/MongoDB) is a good document/object store. [Amazon DynamoDB](https://en.wikipedia.org/wiki/Amazon_DynamoDB) and [Google's Bigtable](https://en.wikipedia.org/wiki/Bigtable) are great examples of wide column stores.\n\n**Use a graph DB** when you're *actually* storing graph data. While you can fit a graph into any database (I've tried), you're going to benefit from graph querying support that comes with a real graph DB. They're optimized for just that use-case.\n\nHaven't had a good excuse to use one yet, but I've heard [Neo4j](https://en.wikipedia.org/wiki/Neo4j) is great.\n\n**My favorite advantage of most NoSQL databases** is their wonderful integration with the JavaScript/TypeScript ecosystem. Most let you store JSON blobs, which means there's no translation between JavaScript objects and database objects.\n\nThat makes your life much easier compared to a SQL-based solution.\n\n### Disadvantages of NoSQL databases\n\nDisadvantages of NoSQL mostly come from its advantages. Funny how that works.\n\nThe simplicity of key:value stores gives you speed at the cost of not being able to store complex data.\n\nThe write speed of document databases comes at the cost of some ACID compliance. Often using the [eventual consistency](https://en.wikipedia.org/wiki/Eventual_consistency) model to write fast and propagate to other instances and objects later.\n\nThe ease of schema-less development comes at the cost of inconsistent data. If you want entries to look the same, you often have to take care of it yourself.\n\nNoSQL databases also struggle with relational data. And it turns out most real world data is relational.\n\nYou *can* store relational data in a NoSQL database, but it can be cumbersome to query. Often there's no support for joins so you have to search through different parts of your database and assemble objects by hand.\n\n### How to use a NoSQL DB\n\nHow to use a NoSQL DB depends on which database you pick. I recommend using an official library.\n\nServerless Handbook focuses on the AWS ecosystem with the serverless framework, so we're going to look at DynamoDB. \n\nDynamoDB is a great choice for saving JSON data, scales well, works fast, and is pretty cheap to use. And unlike RDS, you can set it up using `serverless.yml`.\n\n#### Create a DynamoDB table\n\nTo define a new DynamoDB table, add this to your config.\n\n```yaml\nprovider:\n    environment:\n        DATA_TABLE: ${self:service}-data-${self:provider.stage}\n    iamRoleStatements:\n        - Effect: Allow\n          Action:\n              - dynamodb:Query\n              - dynamodb:Scan\n              - dynamodb:GetItem\n              - dynamodb:PutItem\n              - dynamodb:UpdateItem\n              - dynamodb:DeleteItem\n          Resource: \"arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DATA_TABLE}\"\n```\n\nUsing environment variables for table names makes them easier to use. Postfixing with the stage lets you replicate configuration between development and production without messing up your data.\n\n`iamRoleStatements` give your app permission to use this table.\n\nYou then need to create the actual table:\n\n```yaml\nresources:\n    Resources:\n        DataTable:\n            Type: \"AWS::DynamoDB::Table\"\n            Properties:\n                AttributeDefinitions:\n                    - AttributeName: dataId\n                      AttributeType: S\n                KeySchema:\n                    - AttributeName: dataId\n                      KeyType: HASH\n                ProvisionedThroughput:\n                    ReadCapacityUnits: 1\n                    WriteCapacityUnits: 1\n                TableName: ${self:provider.environment.DATA_TABLE}\n```\n\nYou're defining an AWS resource of type `DynamoDB::Table` with a required `dataId` column that's going to be used as a key. `HASH` keys give you key:value lookups, `RANGE` keys are easier to scan through.\n\nEach table can have 2 keys at most.\n\nName your table using the environment variable defined earlier for consistency. ✌️\n\n#### Save some data\n\nSaving data to a DynamoDB table can be a little cumbersome with Amazon's default SDK. There doesn't seem to be a clear best library so I've been writing small wrappers myself.\n\nYou save data using upserts: If a key exists, the data is updated. If it doesn't, it's created. I recommend using [uuid](https://en.wikipedia.org/wiki/Universally_unique_identifier) for identifiers.\n\nSomething like this:\n\n```typescript\nexport const createData = async (_: any, params: CreateDataParams) => {\n    const dataId = uuidv4();\n\n    const result = await updateItem({\n        TableName: process.env.DATE_TABLE!,\n        Key: {\n            dataId\n        },\n        UpdateExpression: \"SET dataName = :dataName, createdAt = :createdAt\",\n        ExpressionAttributeValues: {\n            \":dataName\": params.dataName,\n            \":createdAt\": new Date().toISOString()\n        },\n        ReturnValues: \"ALL_NEW\"\n    });\n\n    return result.Attributes;\n};\n```\n\nCreate an identifier with the uuid library, run an update expression on the appropriate table. Return the entire new row.\n\n`UpdateExpression` and `ExpressionAttributeValues` is split into two objects to help DynamoDB prevent injection attacks. Also helps with performance.\n\n`updateItem` is a wrapper I built around the official SDK. You can [see the code on GitHub](https://github.com/Swizec/markdownlandingpage.com/blob/master/server/src/dynamodb.ts#L35). I'll turn it into a library once I'm happy with the ergonomics.\n\nYou now have data saved in the database.\n\n**To update this data** you have to create a similar method that gets your `dataId` as a parameter and uses it to run an `updateItem` query. Make sure you aren't always creating a new identifier.\n\n#### Read some data\n\nUnlike with a relational database, you have a choice of *scanning* and *getting*. A scan lets you search for entries that match a criteria. Getting lets you fetch an exact entry.\n\nSomething like this:\n\n```typescript\nexport const allDataWithName = async ({ dataName }) => {\n    const result = await scanItems({\n        TableName: process.env.DATA_TABLE!,\n        FilterExpression: \"#dataName = :dataName\",\n        ExpressionAttributeNames: {\n          \"#dataName\": \"dataName\"\n        },\n        ExpressionAttributeValues: {\n          \":dataName\": dataName\n        }\n    });\n\n    return result.Items;\n};\n```\n\n`scanItems` is again [a little helper method I wrote](https://github.com/Swizec/markdownlandingpage.com/blob/master/server/src/dynamodb.ts#L73). It needs to do more because this is quite cumbersome.\n\nBut it lets you *scan* through a table looking for entries that fit a criteria. \n\nYou can use the `getItem` approach when you know exactly what you're looking for.\n\n```typescript\nexport const data = async ({ dataId }) => {\n    const result = await getItem({ Key: { dataId } });\n\n    if (!result.Item) {\n        return {};\n    }\n\n    return result.Item\n};\n```\n\nNotice how we get a JavaScript object that we can return without modification. That's the beauty of NoSQL. ✌️\n\n## Blockchain\n\n![](../../images/blockchain.png)\n\nBlockchain is the new kid on the block. Usually mixed up with cryptocurrencies and financial speculation, it's actually a solid way to share and store data.\n\nYou've probably used one before 👉 git.\n\nThat's right, [git](https://en.wikipedia.org/wiki/Git) and [The Blockchain](https://en.wikipedia.org/wiki/Blockchain) share the same underlying data structure: a merkle tree.\n\nA [merkle tree](https://en.wikipedia.org/wiki/Merkle_tree) stores data in a cryptographically verified sequence of blocks. Each new block contains a cryptographic hash of the previous block.\n\nThat means you can always verify your data. Follow the chain and validate every hash. Once you reach the initial block, you know your chain is valid.\n\nAs a result you don't need a central authority to tell you the current state of your data. Clients can independently decide, if the data they have is valid. Often by assuming the longest valid chain is correct.\n\nAdding a consensus algorithm makes the process even more robust. When you add new data, how many servers have to agree that the data is valid? \n\nThe result is a slow, but robustly decentralized database. \n\nI wouldn't use the blockchain to store real data just yet, but it's an exciting space to watch. [Blockstack](https://blockstack.org/) is a great way to get started."
  },
  {
    "path": "src/pages/claim.mdx",
    "content": "import { Box } from \"theme-ui\"\nimport { ClaimForm } from \"../components/ClaimForm\"\n\n# Claim your digital copy\n\nAccess interactive features, clickable links, copypasta code, and the latest updates.\n\n<lite-youtube videoid=\"9hhEorVr5Mk\" autoload></lite-youtube>\n\n<br />\n\nHello 👋\n\nYou came here from the back cover of Serverless Handbook, or one of the prompts offering interactive features. I hope that means you're enjoying the book :)\n\nAs promised, the live digital edition is included with your purchase.\n\nSame content as the Kindle or Paperback, except the gifs move, the emojis all work, the code is copypasta, and links are clickable. I know there's some real big links in the book 😅\n\nMy favorite interactive feature right now are the cloud function test widgets embedded in some chapters. Quick way to test what you're building.\n\n## Here's how it works\n\n1. Share a photo of your book on [twitter](https://twitter.com/intent/tweet?text=Learning%20about%20%23serverless%20from%20@swizec's%20new%20Serverless%20Handbook%20and%20...%20https%3A%2F%2Fserverlesshandbook.dev), [facebook](https://facebook.com/sharer.php?u=https%3A%2F%2Fserverlesshandbook.dev), [linkedin](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fserverlesshandbook.dev), or your favorite place to share things\n2. Add a few thoughts\n3. Submit this form 👇\n\n<ClaimForm />\n\n4. You'll get an account on ServerlessHandbook.dev with lifetime access\n\nIf you don't feel like sharing, email me at hi@swizec.com\n\nCheers,<br/>\n~Swizec\n"
  },
  {
    "path": "src/pages/databases/index.mdx",
    "content": "---\ntitle: \"Databases and serverless\"\ndescription: \"Serverless systems don't have a hard drive ... where do you keep your data?\"\nimage: \"./img/databases.png\"\n---\n\n# Where and how to store data\n\n![](../../images/chapter_headers/databases.svg)\n\n_**Serverless systems don't have a hard drive, where does your data go?** You need a database._\n\nBut how do you choose which database and where do you put it? It depends.\n\nEvery database has advantages and disadvantages. Always fit your tech to your problem, not your problem to your tech.\n\n**First, what is a database?** It's [a system for storing and organizing data](https://en.wikipedia.org/wiki/Database). \n\n> A database is an organized collection of data, generally stored and accessed electronically from a computer system.\n\nEvery database technology gives you these features:\n\n- keeps your data\n- lets you query data\n- lets you update data\n\n*Keeping* data is the difference between a [cache](https://en.wikipedia.org/wiki/Cache_(computing)) and a database. You can have an in-memory database for speed, but that doesn't make it a cache.\n\n## How to choose a database\n\nDatabases seek to find a balance between different optimization criteria. Your choice depends on how that balance fits the problem you're solving.\n\nThe common criteria are:\n\n1. Speed of reading data\n2. Speed of writing data\n3. Speed of updating data\n4. Speed of changing the shape of data\n5. Correctness of data\n6. Scalability\n\nNotice how the list is about speed? That's because speed of data access is the biggest predictor of app performance.\n\nI've seen API endpoints hit the database 30+ times. Queries that take 10ms instead of 1ms can mean the difference between a great user experience and a broken app.\n\n### ACID – a database correctness model\n\nWe'll focus on speed in this chapter. But to gain speed and scalability, databases sacrifice correctness. It's important that you know what correctness means in a database context.\n\nTraditional databases follow the [ACID model](https://en.wikipedia.org/wiki/ACID) of transactional correctness. A transaction being a logical operation on your data.\n\n- **Atomicity** ensures that operations inside a transaction succeed or fail together and aren't visible until they all succeed\n- **Consistency** ensures your data is in a valid state and doesn't become corrupted by a half-failed transaction\n- **Isolation** ensures transactions executed in parallel behave the same as if they happened one after another\n- **Durability** ensures that once a transaction succeeds, it stays succeeded and the data doesn't vanish\n\nCertain databases add additional levels of logical correctness on top of the ACID model. We'll talk about those later.\n\n<div id=\"lock\" />\n\nGoals of the ACID model might remind you of the [Architecture Principles](/serverless-architecture-principles) chapter. That's because it aims to guarantee, at the database level, what your serverless architecture aims to ensure at the ecosystem level.\n\nYou're building a glorified database for your web and mobile apps :)\n\n## Types of databases\n\nYou can classify databases into 4 categories based on how they prioritize opposing optimization criteria.\n\n1. **Flat file storage** the simplest and fastest solution, great for large data\n2. **Relational databases** the most correct and surprisingly fast solution, great for complex data\n3. **NoSQL** the class of databases breaking ACID for greater speed/scalability; different types exist\n4. **Blockchain** a distributed database without a central authority, the industry is figuring out what it's good for\n\nRegardless of what you choose, you will talk to your database through the network. It runs on a different machine.\n\nThe network round-trips are a bottomline performance limit. No matter how fast your database, you can't get data faster than it can fly through the network.\n\nServerless providers optimize by running your code as close to your database as possible. If that's not enough, you'll have to run your own servers.\n\n## Flat file database\n\n![](../../images/flat-file.png)\n\nThe simplest way to store data is a [flat file database](https://en.wikipedia.org/wiki/Flat-file_database). You might call it \"organized files\".\n\nFlat files are commonly used for blobby binary data like images. You'll want to put them on S3 for a serverless environment. That negates some of the advantages.\n\n### Advantages of flat files\n\nCompared to other databases, flat files have zero overhead. Your data goes straight to storage without database logic.\n\nThis creates amazing read and write performance. As long as you're adding data to the end of a file, creating a new file, and reading the file start to finish.\n\nA good naming structure gives you fast access to specific files.\n\n### Disadvantages of flat files\n\nFlat files struggle with updates. \n\nTo add a line at the beginning of a file, you have to move the whole thing. To change a line in the middle, you have to update everything that comes after.\n\nThere's no query interface either. You have to read your files to compare, analyze, and search. And without a database, there's no ACID or data shape guarantees.\n\n### When should you store data in flat files\n\nFlat file storage is great when you're looking for speed and simplicity.\n\n**Use flat files when:**\n\n1. You need fast append-only writes\n2. You have simple querying requirements\n3. You read data more often than you write data\n4. You write data that you rarely read\n\n**Avoid flat files when:**\n\n1. You need to cross-reference data or use complex queries\n2. You need fast access across your entire database\n3. Your data changes a lot\n\nNo. 3 is the flat file database killer.\n\nCommon use cases for flat files are logs, large datasets, and binary files (image, video, etc).\n\n*Read about how to use flat files [in the appendix](/appendix-more-databases#flat-file-database)*\n\n## Relational databases – RDBMS\n\n![](../../images/relational_generic.png)\n\n[Relational databases](https://en.wikipedia.org/wiki/Relational_database) are the most common type of database. Data lives in a structured data model and many features exist to optimize performance.\n\nChoosing a relational database for your business data is almost always the right decision. \n\n### Advantages of relational databases\n\nRelational databases have been around since the 1970's. They're battle tested, reliable, and can adapt to almost any workload.\n\nModern systems incorporate popular features from NoSQL like unstructured JSON data. [Postgres](https://en.wikipedia.org/wiki/PostgreSQL) even outperforms NoSQL solutions on certain benchmarks.\n\nThe defining feature of relational databases is the relational data model which lets you model complex data using small isolated concepts. You almost always end up reimplementing this idea with other databases.\n\nAnd after decades of research, relational databases are *fast*.\n\n### Disadvantages of relational databases\n\nRelational databases are harder to use, require more expertise to tune performance, and you lose flexibility. This can be a good thing.\n\nYou can create a database that's fast as lightning, reach a magic number of entries, and performance falls off a cliff. It's hard to horizontally scale a relational database.\n\nBut you *can* make it more flexible with a blobby JSON field on every model. Perfect for metadata.\n\n### When to store data in a relational DB\n\nChoosing a relational database is almost always the correct choice.\n\n**Use relational DBs when:**\n\n1. You don't know how you're using your data\n2. You benefit from data integrity\n3. You need good performance up to hundreds of millions of entries\n4. Your app fits in a single data center (availability zone)\n5. You often use different objects together\n\n**Avoid relational DBs when:**\n\n1. You're storing binary data (images, video)\n2. You don't care about data integrity\n3. You don't want to invest in initial setup\n4. You just need a quick way to save something\n5. You have more data than fits on 1 server\n\nThis makes relational databases the perfect choice for typical applications. You wouldn't use an RDBMS for files, but should consider it for metadata about those files.\n\n*Read about how to use relational databases [in the appendix](/appendix-more-databases#relational-databases--rdbms)*\n\n## The NoSQL approach\n\n![](../../images/nosql.png)\n\n\"NoSQL\" represents a broad range of technologies built for different reasons. A catch-all for any database that isn't relational.\n\nFlat files are a type of NoSQL database.\n\nWikipedia offers [a great description](https://en.wikipedia.org/wiki/NoSQL):\n\n> The data structures used by NoSQL databases are different from those used by default in relational databases, making some operations faster in NoSQL. The particular suitability of a given NoSQL database depends on the problem it must solve.\n\nThis variety is where NoSQL shines. Where relational databases aim to fit many use cases, NoSQL solutions aim to solve a specific problem.\n\n### Flavors of NoSQL\n\nYou can classify NoSQL databases in 4 categories:\n\n1. **key:value store** works like a dictionary. A unique key points to a stored value. Fast read/write performance makes this an ideal caching layer in front of a relational database.\n2. **document store** maps unique keys to documents. Like key:value stores with complex values. Many come with a great query engine.\n3. **graph database** store graph data structures efficiently. Useful for domains with many circular references like social connections and road maps.\n4. **wide column database** act as a mix between a document store and a relational database. Keys map to objects that fit a schema, but the schema isn't prescriptive.\n\nTypical modern databases support multiple models.\n\n### Which NoSQL flavor should you pick?\n\nIt depends. What are you trying to do?\n\nI would prioritize a managed database solution from my serverless provider. Cuts down on networking overhead and makes your life easier because there's one less thing to manage.\n\nThen I would pick what fits my use case.\n\n**Use key:value stores** when you need blazing fast data with low overhead. \n\n**Use a document DB or wide column store** when you want a generic database that isn't relational.\n\n**Use a graph DB** when you're storing graph data.\n\n**My favorite advantage of NoSQL databases** is the wonderful integration with the JavaScript/TypeScript ecosystem. Store JSON blobs, read JavaScript objects.\n\n### Disadvantages of NoSQL databases\n\nDisadvantages of NoSQL stem from its advantages. Funny how that works.\n\nThe simplicity of key:value stores gives you speed at the cost of not being able to store complex data.\n\nThe write speed of document databases comes at the cost of ACID compliance. Often using the [eventual consistency](https://en.wikipedia.org/wiki/Eventual_consistency) model to write fast and distribute later.\n\nThe ease of schema-less development comes at the cost of inconsistent data. NoSQL databases tend to struggle with relations between objects. You can do it, but feels clunky.\n\n*Read more about choosing a NoSQL database and how to use it [in the appendix](/appendix-more-databases#the-nosql-approach-to-data)*\n\n## Blockchain\n\n![](../../images/blockchain.png)\n\nBlockchain is the new kid on the block. Mixed up with cryptocurrencies and financial speculation, it's a solid way to share and store data.\n\nYou've used one before 👉 git.\n\nThat's right, [git](https://en.wikipedia.org/wiki/Git) and [The Blockchain](https://en.wikipedia.org/wiki/Blockchain) share the same underlying data structure: a merkle tree.\n\nA [merkle tree](https://en.wikipedia.org/wiki/Merkle_tree) stores data in a cryptographically verified sequence of blocks. Each block contains a cryptographic hash of the previous block, which means you can verify the whole chain.\n\nAs a result you don't need a central authority to tell you the current state of your data. Each client can decide, if their data is valid. \n\nAdding a consensus algorithm makes the process even more robust. When you add new data, how many servers have to agree that the data is valid? \n\nThe result is a slow, but robustly decentralized database. \n\nI wouldn't use the blockchain in production just yet, but it's an exciting space to watch. [Blockstack](https://blockstack.org/) is a great way to start.\n\n## What should you choose?\n\nProjects tend to use a combination of database technologies.\n\nFiles for large binary blobs, relational database for business data, key:value store for persistent caching, document store for complex data that lives together.\n\nAdding JSON blobs to relational data is a common compromise. ✌️\n\nNext chapter, we look at building a RESTful API for your data."
  },
  {
    "path": "src/pages/dev-qa-prod/index.mdx",
    "content": "---\ntitle: \"Serverless dev, QA, and prod\"\ndescription: \"How do you test and share code without breaking user experience?\"\nimage: \"./img/dev-qa-prod.png\"\n---\n\n# Serverless dev, QA, and prod\n\n![Dev, QA, and prod](../../images/chapter_headers/dev-qa-prod.svg)\n\nYou're building an app and want to show a friend. Do you ship to production?\n\nYou're trying a new feature that doesn't do localhost. Do you publish?\n\nYou've built a pipeline that edits user data and want to make sure it works. Test in production?\n\nIf you're brave enough ... \n\n## Before there's users\n\nNone of this matters until you have users. Build on the main branch, ship to production, test in real life. Enjoy yourself!\n\nCoding at this stage is *fun*. \n\nYou don't have to worry about corrupting user data. No concerns about disrupting a user's workflow. You don't even need to worry about shipping bugs!\n\nIf nobody noticed the bug, was the bug even there?\n\nA word of caution: It's easy to fill your database with crappy data. Try to start production clean. \n\nThank me later when counting users isn't a 5 step process. You'd be surprised how hard it can be to answer *\"How many users do we have?\"*.\n\n## Localhost vs. Production\n\nOnce you have users, you need a way to distinguish production from development. That's easy on a solo project.\n\nLocalhost is for development, production is for production. Run a copy of production on your machine and test.\n\nThe bigger your system, the trickier this gets. You need to host a database, run queues, caching layers, etc.\n\n<div id=\"lock\" />\n\nYou can get close with the [LocalStack plugin for the Serverless Framework](https://www.serverless.com/plugins/serverless-localstack). But only production is like production. ✌️\n\nPlus you can't show off localhost to a friend or coworker.\n\n## The 3 stage split\n\nA common solution to the production vs. development problem is the 3 stage split:\n\n0. localhost\n1. development\n2. staging / QA\n3. production\n\nYou build and test on localhost. Get fast iteration and reasonable certainty that your code works.\n\nWith the Serverless Framework, you can [run lambdas locally](https://www.serverless.com/framework/docs/providers/aws/cli-reference/invoke-local/) like this:\n\n```\nsls invoke local --function yourFunction\n```\n\nYou then push to development. A deployed environment that's like production, but changes lots. Data is irrelevant, used by everyone on the team.\n\nThe development environment helps you test your code with others' work. You can show off to a friend, coworker, or product manager for early feedback.\n\nWhen that works, you push to staging. A more stable environment used to test code right before it ships. Features are production ready, early feedback incorporated.\n\nStaging is the playground for QA and final sign-off from product managers.\n\nThen you push to production. 🚀\n\n### How to use the 3 stage split\n\nInfrastructure-as-code makes the 3 stage split easy to set up. Have 3 branches of your codebase, deploy each to its own stage.\n\nWith the serverless framework, you configure the deploy stage in `serverless.yml`:\n\n```yaml\n# serverless.yml\nservice: my-service\n\nprovider:\n\tname: aws\n\tstage: dev\n```\n\nDeploy with `sls deploy` and that creates or updates the `dev` stage. \n\nStages work via name-spacing. Every resource embeds the stage as part of its name and URL. Keep that in mind when naming resources manually.\n\nLike when naming a queue:\n\n```yaml\n# serverless.yml\nresources:\n  Resources:\n    TimesTwoQueue:\n      Type: \"AWS::SQS::Queue\"\n      Properties:\n        # include the stage variable in your name\n        QueueName: \"TimesTwoQueue-${self:provider.stage}\"\n```\n\nCurrent stage is embedded in the string through the `${self:provider.stage}` variable.\n\n### Dynamic stages\n\nEditing the stage in `serverless.yml` on every deploy is annoying. Pass it in the command line instead.\n\n```yaml\n# serverless.yml\n\nprovider:\n\tname: aws\n\t# use stage option, dev by default\n\tstage: ${opt:stage, \"dev\"}\n```\n\nDeploy with `sls deploy --stage prod` to deploy to production. Defaults to `dev`.\n\nUse a new stage to set up a new environment, existing stage to update. The framework figures it out for you.\n\nMake sure stage names match your [`.env.X` configuration files](/handling-secrets).\n\n## Deploy previews\n\nThe 3 stage split starts breaking down around the 6 to 7 engineers mark. More if your projects are small, less if they're big.\n\nYou start stepping on each other's toes.\n\nAlice is working on a big feature and she'll need 3 months. During that time none of her work can go to production. She'd like to test on development.\n\nBob meanwhile is fixing bugs and keeping the lights on. He needs to merge his work into development, staging, and production every day.\n\nHow can Bob and Alice work together?\n\nThere's 2 solutions:\n\n1. Deploy previews\n2. Feature flags\n\nWith infrastructure-as-code, deploy previews are the simple solution. Create a new stage for every large feature, deploy, show off, and test.\n\nYou get an isolated environment with all the working bits and pieces. Automate it with GitHub Actions to create a new stage for every pull request.\n\nThat's the model Netlify and Vercel promote. Every pull request is automatically deployed on a new copy of production with every update. 👌\n\n## Trunk-based development\n\nA popular approach in large teams is trunk-based development.\n\nEveryone works on the main branch, deploys to production regularly, and uses feature flags to disable features before they're ready. A strong automated testing culture is critical.\n\n[Google uses the Beyonce rule](https://www.oreilly.com/library/view/software-engineering-at/9781492082781/ch01.html):\n\n> If you liked it, you shoulda put a test on it\n\nAnyone can change any code at any time. Tests help you prevent accidents.\n\nFeature flags let you disable new features before they're ready. Your code hits production quickly which ensures it doesn't break. If you refactored something, others can use it. If you created new functionality, it's available.\n\nBut you disable user-facing parts of your feature to avoid a broken experience.\n\nImplementing feature flags can be as easy as an environment variable with a bunch of IF statements, or as complex as progressive canary deploys. Those let you reveal a feature to 1% of users, then 5%, then 10, ...\n\n## Which approach should you pick?\n\nThe best approach depends on team size, established norms, correctness requirements, and your deployment environment.\n\nIf you have a clean infrastructure-as-code approach, creating new stages is great. If you need manual setup, the 3 stage approach is best.\n\nYou can even split your project into sub-projects. Isolated areas of concern that can move and deploy independently. Known as microservices.\n\nAnd remember, the easier your code is to fix and deploy, the less you have to worry about any of this. **Optimize for fast iteration over avoiding mistakes**.\n\nFor side projects I like to test in production. Live wild 🤘\n\nNext chapter, we look at how to think about serverless performance."
  },
  {
    "path": "src/pages/downloads/index.mdx",
    "content": "---\ntitle: \"Downloads\"\ndescription: \"\"\nimage: \"\"\n---\n\n# Download PDF/epub/mobi\n\n<div id=\"lock\" />\n\nHi,\n\nThe beauty of digital books is that you can have moving gifs, copy pastable code, clickable links, interactive features, responsive layouts, blazing fast load times, low battery usage, and offline support.\n\nWebsites are good at that.\n\nBut I get it. You're not into that. You like e-ink and 30meg PDF files. 🤷‍♀️\n\nHere you go:\n\n- [download PDF](https://course-downloadables.s3.amazonaws.com/serverless-handbook/Serverless+Handbook.pdf)\n- [download epub](https://course-downloadables.s3.amazonaws.com/serverless-handbook/Serverless+Handbook.epub)\n- [download mobi](https://course-downloadables.s3.amazonaws.com/serverless-handbook/Serverless+Handbook.mobi)\n\nContent should be the same as on this website as of Mar 25th, 2021. If something's different, pelase let me know.\n\nCheers,<br/>\n~Swizec\n"
  },
  {
    "path": "src/pages/getting-started/index.mdx",
    "content": "---\ntitle: \"Getting Started\"\ndescription: \"Your first introduction to serverless and why you should use it\"\nimage: \"./img/getting-started.png\"\n---\n\nimport { TestCloudFunction } from \"../../components/TestCloudFunctions\"\n\n# Getting Started with Serverless\n\n![](../../images/chapter_headers/getting-started.svg)\n\nHello friend ❤️\n\n> I'm happy you're giving serverless a try. It's one of the most exciting shifts in web development since React introduced us to components.\n\nCreating your first serverless application can be intimidating. Type \"serverless\" into Google and you're hit with millions of results all assuming you know what you're doing.\n\nThere's Serverless, the open source framework, then there's AWS Serverless, and a \"serverless computing\" Wikipedia article, your friends mention lambda functions, then there's cloud functions from Netlify and Vercel ... and aren't Heroku, Microsoft Azure, and DigitalOcean droplets a type of serverless? _\"CloudFlare edge workers!\"_ someone shouts in the background.\n\nIt's all one big mess.\n\n![](giphy:overwhelmed)\n\nThat's why I created the Serverless Handbook. The resource I wish I had :)\n\nLet's start with a short history lesson to get a better understanding of what serverless is and what it isn't. Then you'll build your first serverless backend – an app that says Hello 👋\n\nDon't want the intro? [Jump straight to your first app](/getting-started#your-first-serverless-backend)\n\n## What is serverless\n\n> Serverless is other people's servers running your code.\n\nThe logical next step to platform as a service, which came from The Cloud, which came from virtual private servers, which came from colocation, which came from a computer on your desk running a web server. 🤯\n\n### First we all had servers.\n\n![The world's first web server, a NeXT Computer](../../images/First_Web_Server.jpg)\n\nYou installed Linux on a computer, hooked it up to the internet, begged your internet provider for a static IP address, and let it run 24/7. Mine lived in the bedroom and I'll never forget that IP. Good ol' 193.77.212.100.\n\nWith a static IP address, you can tell [DNS](https://en.wikipedia.org/wiki/Domain_Name_System) servers how to find your server with a domain. People can type that domain into a URL and find your server.\n\nBut a domain doesn't give you a website or a webapp.\n\nFor that, you need to configure Apache or Nginx, set up a reverse proxy to talk to your application, run your application, ensure that it's running and ... it gets out of hand fast. Just to put up a simple website.\n\n### Then came colocation\n\n![A colocation server rack](../../images/Rack001.png)\n\nColocation was a solution for the bedroom problem. What happens if your house catches fire? What if power goes out? Or Mom trips on the power cable and unplugs your computer?\n\nResidential hosting is not reliable.\n\nYour internet is lower tier than a business would get. Less reliable and if the provider needs to do maintenance, they think nothing of shutting off your pipes during non-peak hours. Your server needs strong internet 24/7.\n\nWhen you go on vacation, nobody's there to care for your server. Site might go down for a week before you notice. 😱\n\nColocation lets you take that same server and put it in a data center. They supply the rack space, stable power, good internet, and physical security.\n\nYou're left to deal with configuration, maintenance, and replacing hard drives when they fail.\n\n_PS: Computers break all the time. A large data center replaces a hard drive every few minutes just because a typical drive lasts 4 years and when you have thousands, the stats are not in your favor._\n\nIt's on you to keep everything running.\n\n### Then came virtualization\n\n<div id=\"lock\" />\n\nColocation solved physical problems, but not the fact that your servers are bored.\n\nA typical server runs at about 30% utilization, which means you're wasting money.\n\nReasons for low utilization vary.\n\nYou have to keep the hardware happy and thermally content, you have to over-provision in case of traffic spikes or developer mistakes. Sometimes your site just isn't as popular as you'd like.\n\nWhat if we could run multiple servers on the same machine?\n\n![](giphy:great_idea)\n\nThe first type of virtualization were basic [virtual hosts](https://en.wikipedia.org/wiki/Virtual_hosting). They let you run multiple websites on the same machine. A domain maps to an application on your computer, web server knows the mapping, and voila: sites can share resources.\n\nWorked great but caused problems.\n\nWebsites on the same computer are _very_ close together. You could hack one site and gain access to another. You could starve every website for resources by attacking 1 site with a lot of traffic. You could config yourself into a corner with overlapping configuration.\n\n[Virtual private servers](https://en.wikipedia.org/wiki/Virtual_private_server) and later [containerization](https://en.wikipedia.org/wiki/OS-level_virtualisation) were the solution to that problem. Rather than multiple websites on the same machine, you can host _multiple whole computers_ on the same machine. Like a computer with many brains. 🤯\n\nThe VPS – virtual private server – was born. Providers of \"ssh access\" became popular in the early 2000's. Pay a few bucks a month and you get a real live server on the internet. No hardware required.\n\nYou're on the hook for software setup and you share the machine with other users.\n\nWhat if your site gets popular?\n\n### The cloud is born\n\n![A data center](../../images/photo-1558494949-ef010cbdcc31.jpeg)\n\nEarly VPS was a lot like The Cloud. Computers running on the internet without touching hardware.\n\nWhere VPS struggled was scale.\n\nOnce your traffic started to grow, you'd need more servers to handle the load. There's only so much a single server can do every second.\n\nAnd while computers are getting stronger and stronger (known as [vertical scaling](https://en.wikipedia.org/wiki/Scalability#VERTICAL-SCALING)), it's cheaper to share the load between a lot of small computers ([horizontal scaling](https://en.wikipedia.org/wiki/Scalability#VERTICAL-SCALING)).\n\nBut how do you ensure your servers are all the same? How do you spin them up quickly when traffic spikes on Black Friday?\n\nYou deal with it by hand.\n\nSet up a server, make sure it works. Create a new server. Copy configuration. Create scripts for common tasks and spend hours making sure everything's okay.\n\nRepeat for each new server. 🤮\n\nCloud solves this problem with automation and containers. [Docker](<https://en.wikipedia.org/wiki/Docker_(software)>) for containerization, [kubernetes](https://en.wikipedia.org/wiki/Kubernetes) for orchestration.\n\nYou start every new server from an image in your cloud provider's library. Comes with basic setup and common defaults. You add tweaks and create a new image.\n\nThe cloud provider gives you easy controls to create as many instances of that server as you'd like. Press a button, get a server. ✨\n\nSometimes you can make it scale automatically. Scripts notice traffic rising and create new servers. When traffic subsides, the same scripts tear the servers down.\n\n### Platform as a Service\n\nA variation on The Cloud is the [PaaS](https://en.wikipedia.org/wiki/Platform_as_a_service) – platform as a service.\n\nWith PaaS you pay somebody else to deal with the cloud while you focus on code. They configure your servers and dockers and kubernetes and make everything play together. You build the app.\n\n`git push` to deploy and voila. 👌\n\nMany PaaS providers let you drop down a few levels and break everything. You get to mess with low level configs, operating system libraries, web servers, databases, etc. Empowering _and_ dangerous. I tend to get it wrong.\n\nWhile PaaS takes care of your servers, _you_ have to take care of the \"frontend\". Set up domains and DNS, make your application run right for the platform, configure your own [CDN](https://en.wikipedia.org/wiki/Content_delivery_network), deal with static files, and so on.\n\nThe platform does servers.\n\n### Serverless is born\n\n[Serverless](https://en.wikipedia.org/wiki/Serverless_computing) is the logical next step after PaaS.\n\nOnce you have a system that uses containers to automatically scale and descale based on demand, use repeatable configuration, and painless deploys ... that's serverless right there.\n\nServerless's main innovation are **_tiny_** containers and the ecosystem of services and tools around it.\n\nServer containers so tiny you can spin them up and down in milliseconds. They achieve this because the code they run is:\n\n1. Small\n2. Standardized\n3. Does 1 thing\n\nA serverless \"server\" is a function responding to an API endpoint. Request comes in, server wakes up, runs for a few milliseconds, and goes back to bed.\n\nThe platform takes care of optimization, configuration, and everything else. You get an input and return an output.\n\nServers never idle because they live as long as the request they're serving.\n\nBiggest benefit of this approach?\n\n**Metered pricing.** No more money wasted on idling servers waiting for requests. Pay for the time you're getting work done.\n\n## Your first serverless backend\n\n![](giphy:hello_world)\n\nIn the next few minutes you're going to build your first serverless backend. A service that says Hello 👋\n\nWe're using open source technologies and deploying on AWS Lambda. You can learn about other providers in the [Serverless Flavors](/serverless-flavors) chapter.\n\nYou'll need a computer configured for JavaScript development: Have nodejs installed, a code editor, and a terminal.\n\n### Setup for serverless work\n\nWhen working with serverless I like to use the open source [Serverless](https://github.com/serverless/serverless) framework. We'll talk more about why in the [Good serverless dev experience](/serverless-dx) chapter.\n\nWith the serverless framework we're going to configure servers using YAML files. You write config, framework figures out the rest.\n\nInstall it globally:\n\n```sh\nnpm install -g serverless\n```\n\nYou'll need AWS credentials too.\n\nI recommend following [Serverless's guide on AWS setup](https://serverless.com/framework/docs/providers/aws/guide/credentials/). It walks you through the necessary steps on your Amazon account and a couple terminal commands to run.\n\n### Create a tiny project\n\nThere are no special initializers for serverless projects. You start with a directory and add a configuration file.\n\n![](../../images/start-serverless.gif)\n\n```sh\nmkdir hello-world\ncd hello-world\ntouch serverless.yml\ntouch handler.js\n```\n\nYou now have a project with 2 files:\n\n- `serverless.yml` for configuration\n- `handler.js` for server code\n\nIn future chapters you'll write backends using TypeScript. But one thing at a time :)\n\n### Configure your first server\n\nConfiguration for your server goes in `serverless.yml`. We're telling the Serverless framework that we want to use AWS, run nodejs, and that this is a dev project.\n\nThen we'll tell it where to find the code.\n\n```yaml\n# serverless.yml\n\nservice: hello-world\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: dev\n```\n\nOur service is called `hello-world` and there's a couple details about our provider. The `stage` tells the difference between development, QA, and production deployments. More on that in the [Dev, QA, and prod](/dev-qa-prod) chapter.\n\n#### Let's tell our server how to run code.\n\n```yaml\n# serverless.yml\n\nservice: hello-world\n\nprovider:\n    name: aws\n    runtime: nodejs12.x\n    stage: dev\n\nfunctions:\n    hello:\n        handler: ./handler.hello\n        events:\n            - http:\n\t              path: hello\n\t              method: GET\n\t              cors: true\n```\n\nWe started a `functions` section.\n\nEach entry becomes its own tiny server – a serverless lambda. Together, they're the `hello-world` service.\n\nThe `hello` lambda calls an exported `hello` function inside our `handler.js` file when a GET request hits `/hello`.\n\nAll that from these few lines of code 👌\n\n_PS: enabling [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) lets you call this function from other websites. Like your frontend app._\n\n### Write your first backend function\n\nBackend functions in a serverless environment look like the JavaScript functions you're used to. Grab arguments, return a response.\n\nAdd a hello function to `handler.js`\n\n```javascript\n// handler.js\n\nexports.hello = async (event) => {\n  return {\n    statusCode: 200,\n    body: \"Hello 👋\",\n  }\n}\n```\n\nIt's an async function that accepts a trigger event and returns a response. A success status with a `Hello 👋` body.\n\nThat's it. You wrote backend code. 🤘\n\n### Deploy your first serverless backend\n\nTo deploy, we run `serverless deploy`.\n\n![](../../images/deploy-serverless.gif)\n\nAnd your server is up.\n\nYou get a URL for your lambda and some debugging output. My URL is `https://z7pc0lqnw9.execute-api.us-east-1.amazonaws.com/dev/hello`, if you open it in your browser, it's going to say `Hello 👋`\n\n<TestCloudFunction\n  serviceName=\"serverless-hello-world\"\n  urlPlaceholder=\"https://z7pc0lqnw9.execute-api.us-east-1.amazonaws.com/dev/hello\"\n  defaults\n/>\n\nI'll keep it up because it's free unless somebody clicks. And when they do, current AWS pricing gives me 1,000,000 clicks per month for free 😛\n\n### What you got\n\nThe Serverless framework talked to AWS and configured many things.\n\n![](../../images/hello-world-lambda.png)\n\n- **API Gateway** to proxy requests from the internet to your function\n- **Lambda** to run your code. This is a tiny container that wakes up when called.\n- **CloudWatch logs** to collect logs from your code. Helps with debugging.\n\nAll those are configured for you. No UI to click through, no config to forget about next time, nothing your friends have to set up to deploy the same code.\n\n![](../../images/hello-world.png)\n\nExciting!\n\nNext chapter, we talk about the pros & cons of using serverless in your next project.\n"
  },
  {
    "path": "src/pages/glossary.mdx",
    "content": "export const title = \"Glossary\"\n\n# Glossary\n\nThe Serverless Handbook uses lots of terms that might be unfamiliar. I introduce them at first use and understand new words are hard.\n\nRefer here any time you encounter an unfamiliar word. If something's missing ping me on twitter @swizec.\n\nWords and phrases in alphabetical order.\n\n- **ACID** short for atomicity-consistency-isolation-durability, a model of database correctness\n- **API** short for Application Programming Interface, use to mean everything from the URL structure of a server to the public interface of a class or function\n- **API Gateway** the AWS service that routes HTTP requests from public URLs to internal services\n- **ARN** short for Amazon Resource Name, the globally unique identifier for AWS items\n- **AWS** amazon web services\n- **CDN** short for Content Distribution Network, an approach to serving static files that improves latency\n- **CLI** short for Command Line Interface, the terminal you type commands into\n- **CORS** a security protocol that limits which domains can access certain assets\n- **CPU** short for central processing unit, the part of computers that does most of the work\n- **CRUD** short for Create Read Update Delete, a typical set of operations apps need to support\n- **CloudWatch** the AWS service that collects and displays basic logs and metrics from every other service\n- **DB** short for database\n- **DLQ** short for Dead Letter Queue, a special queue used to hold bad messages for further debugging\n- **DNS** short for Domain Name System/Server, it translates domains to IP addresses\n- **DX** short for developer experience, a catch-all for how it feels to work with a technology\n- **Go** a popular lightweight language for server programming \n- **I/O** short for input/output, the process of reading and writing to an external medium like a hard drive or database\n- **IP address** the globally unique identifier for a computer on the internet\n- **JSON** a popular data format based on JavaScript objects\n- **JVM** short for Java Virtual Machine, the runtime environment for Java applications\n- **QA** short for Quality Assurance, used to mean both the process and the teams doing it\n- **RDS** short for Relational Database Service, an AWS service for hosting relational databases like Postgres and MySQL\n- **S3** the AWS static file hosting service\n- **SNS** short for Simple Notification Service, a messaging pub/sub service on AWS \n- **SQS** short for Simple Queue Service, a messaging queue service on AWS\n- **SSL** the encrypted communication layer used on the web\n- **UI** short for user interface\n- **VPS** short for Virtual Private Server, a type of shared hosting\n- **apache** a common web server popular with the open source community\n- **azure** Microsoft's cloud and serverless computing platform\n- **blockchain** a distributed ledger used for cryptocurrencies, data storage, and smart contracts\n- **cache** a fast data storage service or app for temporary data used to improve performance\n- **chrome puppeteer** a library used to automate browser tasks with Chrome\n- **cloud function** another name for a unit in function-as-a-service serverless computing\n- **cloudfront** AWS's CDN service\n- **compute** a fuzzy term for unit of computation used to talk about pricing and performance\n- **devops** is a set of practices that combines development and IT operations\n- **docker** a popular computer virtualization software and toolkit\n- **dynamodb** a wide-table NoSQL database service on AWS\n- **edge workers** a type of cloud function that works like a CDN and aims to reduce latency\n- **exponential backoff** a common approach to reducing load on a 3rd party service that is struggling\n- **firebase** Google's suite of backend-as-a-service offerings\n- **git** a common version control system\n- **graphql** an API protocol based on queries commonly used to  increase client power and flexibility\n- **hashing function** a secure method of uniquely encoding a string in a way that cannot be reversed\n- **heroku** a popular platform-as-a-service provider\n- **http** short for HyperText Transfer Protocol, the underlying protocol of the web\n- **https** the secure encrypted version of http that uses SSL\n- **jamstack** Javascript And Markdown stack, a stack of technologies used for modern static-first websites\n- **kubernetes** a popular toolkit for managing servers and containers\n- **lambda function** a unit in function-as-a-service serverless computing, synonym for cloud function\n- **netlify** a jamstack and serverless provider, popularized the jamstack approach\n- **nginx** a popular web server created in 2004 to address Apache's performance issues\n- **nosql** an umbrella term for databases that trade ACID compliance for specific performance or usability gains\n- **PaaS/platform as a service** a type of hosting that aims to solve infrastructure complexity\n- **poison pill** an unprocessable message or request\n- **queue** a data structure that processes messages in order, also used as shorthand for a service or application that acts as a queue\n- **rainbow tables** a pre-computed cache of hashes used to reverse hashed passwords\n- **relational database** a popular approach to storing business data since the 1970's, often synonymous with the concept of a database\n- **rest** a common approach to designing web APIs\n- **server** depending on context, a machine running software that serves internet requests, or the software itself\n- **serverless** an on-demand hosting environment\n- **ssh** short for secure shell, a protocol for remotely controlling services\n- **upsert** an operation that inserts a new object or updates an existing object with the same identifier\n- **vercel** a jamstack and serverless provider\n- **yak shaving** a useless activity that indirectly helps you solve a larger problem\n\n"
  },
  {
    "path": "src/pages/handling-secrets/index.mdx",
    "content": "---\ntitle: \"Handling Secrets\"\ndescription: \"Talking to 3rd party APIs is where Serverless shines. Which of the 3 ways to handle secrets should you choose?\"\nimage: \"./img/handling-secrets.png\"\n---\n\n# Handling Secrets\n\n![](../../images/chapter_headers/handling-secrets.svg)\n\nHow do you send an SMS when users click a button?\n\nYou find a JavaScript library that talks to an SMS provider. Configure your API keys, call the library, user gets an SMS. Yay!\n\n3 months later you wake up to a $5,000 bill. Someone looked at your JavaScript code, took the API keys, and ran a spam campaign.\n\n![](giphy:oops)\n\nOrchestrating 3rd party services is where cloud functions shine. The perfect environment for glue code.\n\nIsolated code that does *one* thing with no cruft. Runs on-demand, consumes no resources when not in use, scales near infinitely. Perfection.\n\n**And it runs on a server where users can't see the code.** There's no right-click inspect, no JavaScript files downloaded, no user environment at all.\n\n😍\n\n## What is a secret\n\nA secret is any piece of information you can't share. Any key with access to a special resource. Passwords and API tokens, for example. \n\nYou can add semi-secret configuration variables. URLs for parts of your system, ports of a database server, kinda-hardcoded data, etc.\n\nHow secretive you have to be depends on context.\n\nConfiguration variables are okay to leak, if the system is otherwise secure. But they can give an attacker information about your system.\n\nProduction passwords for sensitive health information ... you don't even want your engineers to know those. Especially not former engineers.\n\n## 3 ways to handle secrets\n\nThere are 3 ways to handle secrets. From least to most secure.\n\n1. Hardcoded values\n2. Dotenv files\n3. Secrets manager\n\nEach method comes with different pros and cons. Pros in terms of security, cons in how cumbersome to use.\n\n<div id=\"lock\" />\n\n## Hardcoded values in code\n\n```typescript\nMY_SECRET_KEY=\"f3q20-98facv87432q4\"\n```\n\nHardcoded secrets are the easiest to use and the least secure.\n\nThey're okay for prototyping. Reduce moving pieces and focus on the API integration. Ignore the yak shaving around your goal.\n\nCode runs on the server and users won't be able to steal your secrets.\n\n**But anyone with access to your code can see the secrets.** \n\nShare on GitHub and that includes the whole world. Bots always scrape GitHub looking for strings that look like keys. Your secret *will* be stolen.\n\nAWS is paranoid enough that their own bot looks for secret keys. If they find yours, your AWS account gets locked. [Ask me how I know](https://swizec.com/blog/what-happens-when-you-push-aws-credentials-to-github/) 😅\n\nAnother issue with hardcoded keys is that they're hard to change. You have to re-deploy every time. And you're forced to use the same account for testing and production.\n\n## Dotenv files\n\nA step up from hardcoded keys are dotenv files – `.env`. Configuration files in your codebase that hold secrets.\n\n```bash\n# .env\nMY_SECRET_KEY=f3q20-98facv87432q4\nMY_API_URL=https://example.com\n```\n\nA `.env` file holds your secrets and configuration variables in one place. Makes them easier to use and change without searching through code.\n\n**You should *not* store these in version control.** That's where the increased security comes from. \n\nThe common approach is to:\n\n1. Have a blank `.env` file with every variable stored in version control\n2. Every engineer makes a copy\n3. Fills out values from team members or a shared passwords manager\n\nYou'll never leak secrets to GitHub by accident. But they're unencrypted on everyone's laptop, difficult to change across a large team, and packaged into your deploys.\n\nAnyone who breaks into your laptop or steals a deploy package from S3 can read the secrets.\n\nOn the bright side, dotenv files are easy to split between environments. You can have `.env.local`, `.env.production`, `.env.development` with different values for every secret. ✌️\n\n### How to use .env files\n\nMany frameworks support `.env` files by default. Populate the file and read values from `process.env`.\n\nWhen using the Serverless Framework, you'll need a plugin: `serverless-dotenv-plugin`. Here's what you do.\n\nInstall the plugin:\n\n```\nyarn add serverless-dotenv-plugin\n```\n\nEnable it in your serverless.yml config:\n\n```yaml\n# serverless.yml\nplugins:\n  - serverless-dotenv-plugin\n```\n\nRun deploy and access values with `process.env` 🤘\n\nYou can match environment specific files to deployments using the `stage: X` config. serverless-dotenv-plugin reads the `.env.X` file that matches your stage.\n\n## Secrets manager\n\nThe most secure way to handle secrets is using a secrets manager.\n\nA secrets manager works like the password manager in your browser. You have to authenticate to get access, re-authenticate *every time*, and secrets are encrypted when not in use.\n\nYou can even make your secrets double blind. *Nobody* needs to know their values.\n\nEngineers can't see secrets in the code, they're not saved on anyone's laptop, you can't steal them from the server, and with the right configuration, secrets change every N days.\n\n![](giphy:secure_vault)\n\n### How to use a secrets manager\n\nIf you're on Netlify or Vercel, their secrets system is a secrets manager. They control the run-time and inject those values into `process.env`.\n\nOn AWS, you'll have to partially build your own.\n\n**First**, save your secrets in [AWS Secrets Manager](https://console.aws.amazon.com/secretsmanager/home). Follow the wizard, it's great.\n\n![AWS Secrets Manager configuration](../../images/secrets-manager.png)\n\nYou can store many secret values in 1 configuration. I recommend grouping by environment – dev, staging, production – or use a logical grouping based on what you're doing. One secret per API.\n\n**Second**, give your code permission to access secrets.\n\n```yaml\n# serverless.yml\nprovider:\n  # ...\n  iamRoleStatements:\n    - Effect: \"Allow\"\n      Action:\n        - \"secretsmanager:GetSecretValue\"\n      Resource: \"arn:aws:secretsmanager:${self:provider.region}:*\"\n```\n\nYou're giving permission to `GetSecretValue`, *not* to make changes. This is important. You do not want somebody hacking into your system and locking you out.\n\nUsing an asterisk – `*` – for secret name is convenient. For more security, limit access to specific secrets.\n\n**Third**, access your secrets at runtime.\n\n```typescript\nimport { SecretsManager } from \"aws-sdk\"\n\nconst ssm = new SecretsManager({\n  region: \"us-east-1\", // make sure this matches your region\n})\n\nconst secret = await ssm\n  .getSecretValue({ SecretId: \"<your secret name>\" })\n  .promise()\n\nconst { MY_SECRET_KEY, MY_API_URL } = JSON.parse(secret?.SecretString)\n```\n\nThis instantiates a new SSM client, gets your secret value, returns a JSON. Parse JSON, get secrets.\n\nThis is [an API call that might fail](https://serverlesshandbook.dev/robust-backend-design). Make sure to handle errors and fail correctly, if you can't get the secret.\n\nYou'll have to do this every time.\n\nFetching a fresh secret every time ensures that:\n\n1. you get the latest value\n2. you don't keep decrypted secrets in memory for long\n\nHowever, this increases latency. You're making an API call. Not a lot, but not zero. \n\nYou can memoize secrets values in Lambda memory. Rely on the ephemeral nature of your environment to forget.\n\n## Conclusion\n\nChoose the strategy that fits your use-case and safety needs.\n\nI like to hardcode development values in the tinkering phase and *change keys afterwards*. You never know what you leaked.\n\nWhen my code's ready, I put configuration values in `.env` files and secrets in Secrets Manager.\n\nNext chapter we look at authentication."
  },
  {
    "path": "src/pages/index.mdx",
    "content": "import { graphql } from \"gatsby\"\nimport {\n  Box,\n  Flex,\n  Button,\n  Container,\n  Heading,\n  Grid as ThemeUIGrid,\n} from \"theme-ui\"\nimport {\n  Avatar,\n  Banner,\n  Grid,\n  GumroadButton,\n  GumroadOverlay,\n  TinyFormCK,\n  Testimonial,\n} from \"@swizec/gatsby-theme-course-platform\"\nimport { NavGrid, ChapterHeading, HomeTitle } from \"../components/homepage\"\n\nexport const pageQuery = graphql`\n  query {\n    ThaiWood: file(relativePath: { eq: \"testimonial-avatars/thaiwood.png\" }) {\n      childImageSharp {\n        fluid(maxWidth: 110, maxHeight: 110) {\n          ...GatsbyImageSharpFluid\n        }\n      }\n    }\n    AlNotara: file(relativePath: { eq: \"testimonial-avatars/alnotara.jpg\" }) {\n      childImageSharp {\n        fluid(maxWidth: 110, maxHeight: 110) {\n          ...GatsbyImageSharpFluid\n        }\n      }\n    }\n    MarekC: file(relativePath: { eq: \"testimonial-avatars/MarekC.png\" }) {\n      childImageSharp {\n        fluid(maxWidth: 110, maxHeight: 110) {\n          ...GatsbyImageSharpFluid\n        }\n      }\n    }\n    AdamRackis: file(\n      relativePath: { eq: \"testimonial-avatars/adamrackis.jpeg\" }\n    ) {\n      childImageSharp {\n        fluid(maxWidth: 110, maxHeight: 110) {\n          ...GatsbyImageSharpFluid\n        }\n      }\n    }\n    DavidWells: file(\n      relativePath: { eq: \"testimonial-avatars/davidwells.jpeg\" }\n    ) {\n      childImageSharp {\n        fluid(maxWidth: 110, maxHeight: 110) {\n          ...GatsbyImageSharpFluid\n        }\n      }\n    }\n  }\n`\n\n<Banner>\n\n<HomeTitle />\n\n<Box sx={{ my: \"auto\", mb: 4 }} />\n\n<ChapterHeading sx={{ mb: 2 }} />\n\n<NavGrid>\n\n- [Getting Started](/getting-started)\n- [Serverless Pros & Cons](/serverless-pros-cons)\n- [Choosing Providers](/serverless-flavors)\n- [Create Good serverless DX](/serverless-dx)\n- [Architecture principles](/serverless-architecture-principles)\n- [Lambdas, queues, etc](/serverless-elements)\n- [Robust backend design](/robust-backend-design)\n- [Where to store data](/databases)\n- [Creating a REST API](/serverless-rest-api)\n- [Using GraphQL](/serverless-graphql)\n- [Lambda pipelines](/lambda-pipelines)\n- [Monitoring ](/serverless-monitoring)\n- [Dev, QA, and prod](/dev-qa-prod)\n- [Serverless performance](/serverless-performance)\n- [Using Chrome puppeteer](/serverless-chrome-puppeteer)\n- [Handling secrets](/handling-secrets)\n- [Dealing with authentication](/serverless-authentication)\n- [Glossary](/glossary)\n- [Appendix: More databases](/appendix-more-databases)\n\n</NavGrid>\n\n</Banner>\n\n<Container sx={{py: 3}}>\n\n<Testimonial\n  quote={\n    <>\n      I have really enjoyed the approach you've taken with the book. It's got me\n      thinking practically about Serverless as a whole, but more specifically\n      each aspect that contributes to a robust implementation of Serverless for\n      production. <b>I read a third of the book the first night I had it!</b>\n    </>\n  }\n  image={props.data.AlNotara.childImageSharp.fluid.src}\n  name=\"Al Notara\"\n  company=\"freelance web developer\"\n/>\n\nHello! 👋\n\nAre you a frontend engineer diving into backend? Do you have just that one bit of code that can't run in the browser? Something that deals with secrets and APIs?\n\nhttps://www.youtube.com/watch?v=udqyBqCgLrU\n\nThat's what cloud functions are for my friend. You take a JavaScript function, run it on serverless, get a URL, and voila.\n\nBut that's easy mode. Any tutorial can teach you that.\n\n**What happens when you wanna build a real backend?** When you want to understand what's going on? Have opinions on REST vs GraphQL, NoSQL vs. SQL, databases, queues, talk about performance, cost, data processing, deployment strategies, developer experience?\n\n🤯\n\n<Testimonial\n  quote={\n    <>\n      <p>\n        Serverless Handbook is a wonderfully written introduction to serverless.\n        It walks through serverless design principles and constraints, and\n        discusses the various providers, and their tradeoffs. It moves on to\n        cover a broad overview of the most common aws services, and how to use\n        them with the Serverless framework.\n      </p>\n      <p>\n        This book is a must-read for anyone new to serverless tech, looking to\n        get up to speed.\n      </p>\n    </>\n  }\n  image={props.data.AdamRackis.childImageSharp.fluid.src}\n  name=\"Adam Rackis\"\n  company=\"engineer at Spotify\"\n/>\n\n## Get your free chapter!\n\nAccess to this chapter immediately, extra free chapter and Serverless crash course in your email ✌️\n\n<TinyFormCK\n  copyBefore=\"\"\n  submitText=\"Send crash course! 💌\"\n  onSuccess={() => setTimeout(props.unlockCurrentPage, 2000)}\n/>\n<br />\n\n[![](../images/buy-now-amazon.png)](https://geni.us/serverless-handbook)\n\n## Dive into modern backend. Understand any backend\n\n<p style={{ fontSize: \"1.1em\" }}>\n  <strong>Serverless Handbook</strong> shows you how with{\" \"}\n  <strong>360 pages</strong> for people like you getting into backend\n  programming.\n</p>\n\nWith **digital + paperback content** Serverless Handbook has been more than 1 year in development. Lessons learned from 14 years of building production grade websites and webapps.\n\n<Testimonial\n  quote=\"With Serverless Handbook, Swiz teaches the truths of distributed systems – things will fail – but he also gives you insight on how to architect projects using reliability and resilience perspectives so you can monitor and recover.\"\n  image={props.data.ThaiWood.childImageSharp.fluid.src}\n  name=\"Thai Wood\"\n  company=\"author of Resilience Roundup\"\n/>\n\nIf you want to understand backends, grok serverless, or just get a feel for modern backend development, this is the book for you.\n\nServerless Handbook full of **color illustrations**, **code you can try**, and **insights you can learn**. But it's not a cookbook and it's not a tutorial.\n\n[![Serverless Handbook on your bookshelf](../images/serverless-handbook-bookshelf.jpg)](https://geni.us/serverless-handbook)\n\nYes, there's a couple tutorials to get you started, to show you how it fits together, but the focus is on high-level concepts.\n\n<p style={{ fontSize: \"1.1em\" }}>\n  <strong>Ideas</strong>, <strong>tactics</strong>, and{\" \"}\n  <strong>mindsets</strong> that you need. Because every project is different.\n</p>\n\nThe Serverless Handbook takes you _from your very first cloud function to modern backend mastery_. In the words of an early reader:\n\n<Testimonial\n  quote={\n    <>\n      <p>\n        Serverless Handbook taught me high-leveled topics. I don't like recipe\n        courses and these chapters helped me to feel like I'm not a total noob\n        anymore.\n      </p>\n      <p>\n        The hand-drawn diagrams and high-leveled descriptions gave me the\n        feeling that I don't have any critical \"knowledge gaps\" anymore.\n      </p>\n    </>\n  }\n  image={props.data.MarekC.childImageSharp.fluid.src}\n  name=\"Marek C.\"\n  company=\"frontend engineer\"\n/>\n\n>\n\nIf you can JavaScript, you can backend.\n\nPlus it looks great on your bookshelf 😉\n\n[![](../images/buy-now-amazon.png)](https://geni.us/serverless-handbook)\n\nCheers,<br/>\n~Swizec\n\n<Grid gap={2} sx={{ fontWeight: 'bold', py: 3 }}>\n\n- Get started with Serverless backend technologies\n- Build your first backends, or your 10th in a new way ;)\n- Designed for frontend engineers diving into backend tech for the first time\n- Learn all about lambdas and queues and databases\n- Build server-side technologies with JavaScript and TypeScript\n- Support your apps with functions in the cloud\n- A reference handbook that stands by your side as you work\n- Pick and choose, read the chapters you need right now\n- Access yours forever\n- Live digital version with your Kindle or Paperback\n- Available wherever Amazon sends books\n\n</Grid>\n\n[![](../images/buy-now-amazon.png)](https://geni.us/serverless-handbook)\n\n<Testimonial\n  quote={\n    <>\n      <p>\n        This handbook is a fantastic overview of what it means to embrace\n        serverless technology. After explaining what the heck \"serverless\"\n        really means, the book digs deep into the core elements of serverless in\n        an easy-to-follow illustrative manner with ready deployable code!\n      </p>\n      <p>\n        After learning about all the primitives (functions/queues/s3/etc.) you'd\n        need as a serverless dev, the book also covers other critical\n        operational aspects like observability, multi-staged deployments &\n        handling secrets.\n      </p>\n      <p>\n        It's a great book on using infrastructure-as-code tools like the\n        serverless framework to achieve more with less.\n      </p>\n      <p>\n        I'm highly impressed by Swizecs's approach to teaching devs how to adopt\n        new serverless technology.\n      </p>\n    </>\n  }\n  image={props.data.DavidWells.childImageSharp.fluid.src}\n  name=\"David Wells\"\n  company=\"Serverless Framework core contributor\"\n/>\n\n[![](../images/buy-now-amazon.png)](https://geni.us/serverless-handbook)\n\n# What's in Serverless Handbook?\n\n360 pages, 19 chapters, 6 full projects, hand-drawn diagrams, beautiful chapter art, best-looking cover in tech. ✌️\n\nWatch the walkthrough or keep reading.\n\nhttps://www.youtube.com/watch?v=WC-mEf4K9as&t=1772s\n\n**Getting Started** walks you through the history of servers and _why_ serverless exists.\n\n**Serverless Pros & Cons** helps you make informed decisions. Does serverless fit your project?\n\n**AWS, Azure, Vercel, Netlify, or Firebase?‌** talks about choosing a provider and their tradeoffs.\n\n**Good serverless DX** is about developer experience and how serverless helps you improve yours.\n\n**Architecture principles** talks about distributed architectures and what you'll want to think about.\n\n**Lambdas, queues, etc** explains the core building blocks of serverless backends and how you can use them.\n\n**Robust backend design** continues with ideas on resilience and reliability.\n\n**Where to store data** is all about different databases, their tradeoffs, and how they work. A core aspect of backend.\n\n**Creating a REST API** explains what is REST and what it isn't, how to design a good API, and walks you through a tutorial with working code.\n\n**Using GraphQL** shows you the benefits of GraphQL, when you should use it, and shares a tutorial where you deploy a serverless GraphQL API.\n\n**Lambda pipelines** talks about distributed data processing at scale and how you might use serverless for the task. Great dive into robust architecture design.\n\n**Monitoring serverless apps** gives you ideas on observability and how to ensure you'll know something's funky before users start yelling.\n\n**Dev, QA, and prod** shares common strategies for scaling as a team, avoiding mishaps, and when you should use which.\n\n**Serverless performance** touches cost optimization and focuses on how to keep your system fast and performant.\n\n**Serverless Chrome puppeteer** was hard to fit in the book, but it's such a darling I couldn't let go. Shows you how to run Serverless Chrome for browser automation.\n\n**Handling secrets** is an important topic on how to make sure your system doesn't get hacked. Leaking API keys is a common oopsie.\n\n**Dealing with authentication** talks about authentication, why it's hard, and shows you how to roll your own or use a 3rd party provider.\n\n**Glossary** defines commonly used words so you can look them up as you work.\n\n**Appendix: More databases** is everything you ever wanted to know about different databases and how to use them.\n\nPlus, Serverless Handbook _really_ looks great on your desk 😊\n\n[![Serverless Handbook on your desk](../images/serverless-on-desk.jpg)](https://geni.us/serverless-handbook)\n\n[![](../images/buy-now-amazon.png)](https://geni.us/serverless-handbook)\n\n# Frequently Asked Questions\n\n## Why Amazon?\n\nShe's a good book Friend and she deserves to hit the bestseller list.\n\nhttps://twitter.com/Swizec/status/1377377288895918082\n\nhttps://twitter.com/Swizec/status/1377618407869325320\n\n🤘\n\n## $9.99 for Kindle are you sure? Your book is worth more.\n\nNot a fake question, I promise.\n\n![](../images/kindle-price-question.png)\n\nAmazon decided that kindle books should be $9.99. You get a 70% margin. Choose higher and your margin falls to 30%.\n\nThat means a $22 and a $9.99 book make the same revenue for the author. In that case making [Serverless Handbook](https://serverlesshandbook.dev) more accessible is the better choice 😊\n\n## $49 for the paperback does it read itself??\n\nAlso not a fake question 😂\n\nhttps://twitter.com/dxvgx/status/1377378042750844930\n\nYou know how most books you buy _aren't_ color and look like they were printed on toilet paper? This week I learned why.\n\n![Paperback royalty structure](../images/paperback-royalties.png)\n\nThat's right, it costs $26 to print each copy of [Serverless Handbook](https://serverlesshandbook.dev). Add distribution costs and Amazon overhead ...\n\nI didn't want you to read toilet paper.\n\n## Amazon doesn't show your book in my country\n\nSorry that's out of my hands 😔 I clicked all the buttons and flipped all the checkboxes and said _\"Widest distribution possible please!\"_\n\nYou can [try buying from Amazon's US store directly](https://www.amazon.com/dp/0578887916)\n\n## I don't like Amazon, don't have Kindle, and killing trees is bad. How can I read Serverless Handbook?\n\nYou can buy the [live digital version on Gumroad](https://gum.co/NsUlA). Otherwise included with kindle/paperback purchase :)\n\n<Box sx={{ mt: 10, textAlign: \"center\" }}>\n  <a\n    className=\"gumroad-button\"\n    href=\"https://gum.co/NsUlA\"\n    data-gumroad-single-product=\"true\"\n    target=\"_blank\"\n    rel=\"noopener noreferrer\"\n  >\n    Get Serverless Handbook Digital\n  </a>\n</Box>\n\n# Serverless Handbook in the wild\n\n</Container>\n\n<ThemeUIGrid gap={2} columns={[1, 3]} sx={{ px: [1, 3]}}>\n\nhttps://twitter.com/rachelnabors/status/1564431223740846080\n\nhttps://twitter.com/octane88/status/1523027838919593985\n\nhttps://twitter.com/bahit/status/1506844944442728452\n\nhttps://twitter.com/SammyTeahan/status/1466133105610133506\n\nhttps://twitter.com/devanfarrell_/status/1463025330545852420\n\nhttps://twitter.com/dudupeer/status/1460130286528876546\n\nhttps://twitter.com/erikaybar_/status/1445068134646378501\n\nhttps://twitter.com/raae/status/1405144389920432136\n\nhttps://twitter.com/OlaHolstVea/status/1405158531007930369\n\nhttps://twitter.com/opagani/status/1383293337029922820\n\nhttps://twitter.com/dxvgx/status/1383209549809602570\n\nhttps://twitter.com/flybayer/status/1383058155555393541\n\nhttps://twitter.com/juanggiraldo/status/1382739132758290434\n\nhttps://twitter.com/Kosai106/status/1381551022464765955\n\nhttps://twitter.com/chagwood/status/1381368867289763846\n\nhttps://twitter.com/Ryan_Magoon/status/1381053872370814979\n\nhttps://twitter.com/code_e_averett/status/1380573360300646403\n\nhttps://twitter.com/AdamRackis/status/1379184860586184704\n\nhttps://twitter.com/TheAppyWriter/status/1378688644987371526\n\nhttps://twitter.com/LukyVJ/status/1384835578932387841\n\nhttps://twitter.com/mikedubcurry/status/1387920478166265857\n\nhttps://twitter.com/AdamRackis/status/1388192278884331530\n\nhttps://twitter.com/grepliz/status/1389677853013286913\n\nhttps://twitter.com/royanger/status/1389976070959640578\n\nhttps://twitter.com/__MateN/status/1390360722299817987\n\nhttps://twitter.com/ErstonGreatman/status/1392229831203561472\n\n</ThemeUIGrid>\n"
  },
  {
    "path": "src/pages/lambda-pipelines/index.mdx",
    "content": "---\ntitle: \"Lambda pipelines for serverless data processing\"\ndescription: \"Build a robust massively distributed data, event, or message processing pipeline\"\nimage: \"./img/lambda-workflows.png\"\n---\n\n# Lambda pipelines for serverless data processing\n\n![](../../images/chapter_headers/lambda-workflows.svg)\n\nYou get tens of thousands of events per hour. How do you process that?\n\nYou've got a ton of data. What do you do?\n\nUsers send hundreds of messages per minute. Now what?\n\nYou could learn Elixir and Erlang – purpose built languages for message processing used in networking. But is that where you want your career to go?\n\nYou could try [Kafka](https://kafka.apache.org/) or [Hadoop](https://hadoop.apache.org/). Tools designed for big data, used by large organizations for mind-boggling amounts of data. Are you ready for that?\n\nElixir, Erlang, Kafka, Hadoop are wonderful tools, if you know how to use them. But there's a significant learning curve and devops work to keep them running.\n\nYou have to maintain servers, write code in obscure languages, and deal with problems we're trying to avoid.\n\n## Serverless data processing\n\nInstead, you can leverage existing skills to build a data processing pipeline.\n\nI've used this approach to process millions of daily events with barely a 0.0007% loss of data. A rate of 7 events lost per 1,000,000.[^1]\n\nWe used it to gather business and engineering analytics. A distributed `console.log` that writes to a central database. That's how I know you should never build a distributed logging system unless it's your core business 😉\n\n![High level architecture for distributed logging and tracing](../../images/distributed-logging.png)\n\nThe system accepts batches of events, adds info about user and server state, then saves each event for easy retrieval.\n\nIt was so convenient, we even used it for tracing and debugging in production. Pepper your code with `console.log`, wait for an error, see what happened.\n\n![](giphy:perfection)\n\nA similar system can process almost anything.\n\nGreat for problems you can split into independent tasks like prepping data. Less great for large inter-dependent operations like machine learning.\n\n## Architectures for serverless data processing\n\nServerless data processing works like `.map` and `.reduce` at scale. Inspired by Google's infamous [MapReduce programming model](https://en.wikipedia.org/wiki/MapReduce) and used by big data processing frameworks.\n\nWork happens in 3 steps:\n\n1. Accept chunks of data\n2. **Map** over your data\n3. **Reduce** into output format\n\nSay you're building an adder: multiply every number by 2 then sum.\n\n<div id=\"lock\" />\n\nUsing functional programming patterns in JavaScript, you'd write code like this:\n\n```javascript\nconst result = [1, 2, 3, 4, 5] // input array\n  .map(n => n * 2) // multiply each by 2\n  .reduce((sum, n) => sum + n, 0) // sum together\n```\n\nEach step is independent.\n\nThe `n => n*2` function only needs `n`. The `(sum, n) => sum+n` function needs the current `sum` and `n`.\n\nThat means you can distribute the work. Run each on a separate Lambda in parallel. Thousands at a time.\n\nYou go from a slow algorithm to as fast as a single operation, known as [Amdahl's Law](https://en.wikipedia.org/wiki/Amdahl%27s_law). With infinite scale, you could process an array of 10,000,000 elements almost as fast as an array of 10.\n\n![](giphy:mind_blown)\n\nLambda limits you to 1000 parallel invocations. Individual steps can be slow (like when transcoding video) and you're limited by the `reduce` step.\n\nPerformance is best when reduce is un-necessary. Performance is worst when reduce needs to iterate over every element in 1 call and can't be distributed.\n\nOur adder is [commutative](https://en.wikipedia.org/wiki/Commutative_property) and we can parallelize the reduce step using chunks of data.\n\n![Distributed adder architecture](../../images/processing-architecture.png)\n\nIn practice you'll find transformations that don't need a reduce step or require many maps. All follow this basic approach. :)\n\nFor the comp sci nerds 👉 this has no impact on [big-O complexity](https://en.wikipedia.org/wiki/Big_O_notation). You're changing real-world performance, not the algorithm.\n\n## Build a distributed data processing pipeline\n\nLet's build that adder and learn how to construct a robust massively distributed data processing pipeline. We're keeping the operation simple so we can focus on the architecture.\n\nHere's the JavaScript code we're distributing:\n\n```javascript\nconst result = [1, 2, 3, 4, 5] // input array\n  .map((n) => n * 2) // multiply each by 2\n  .reduce((sum, n) => sum + n, 0) // sum together\n```\n\nFollowing principles from the [Architecture Principles](/serverless-architecture-principles) chapter, we're building a system that is:\n\n- easy to understand\n- robust against errors\n- debuggable\n- replayable\n- always inspectable\n\nWe're going to use 3 [Serverless Elements](/serverless-elements) to get there:\n\n1. lambdas\n2. queues\n3. storage\n\nYou can see the [full code on GitHub](https://github.com/Swizec/serverlesshandbook.dev/tree/master/examples/serverless-data-pipeline-example).\n\n### The elements\n\nWe're using 3 lambdas, 2 queues, and 2 DynamoDB tables.\n\n**3 Lambdas**\n\n1. `sumArray` is our API-facing lambda. Accepts a request and kicks off the process\n2. `timesTwo` is the map lambda. Accepts a number, multiplies by 2, and triggers the next step.\n3. `reduce` combines intermediary steps into the final result. It's the most complex.\n\nOur lambdas are written in TypeScript and each does 1 part of the process.\n\n**2 Queues**\n\n1. `TimesTwoQueue` holds messages from `sumArray` and calls `timesTwo`.\n2. `ReduceQueue` holds messages from `timesTwo` and calls `reduce`, which also uses the queue to trigger itself.\n\nWe're using [SQS – Simple Queue Service – queues](https://aws.amazon.com/sqs/) for their reliability. An SQS queue stores messages for up to 14 days and keeps retrying until your Lambda succeeds.\n\nWhen something goes wrong there's little chance a message gets lost unless you're eating errors. If your code doesn't throw, SQS interprets that as success.\n\nYou can configure max retries and how long a message should stick around. When it exceeds those deadlines, you can configure a Dead Letter Queue to store the message.\n\nDLQ's are useful for processing bad messages. Send an alert to yourself, store it in a different table, debug what's going on.\n\n**2 tables**\n\n1. `scratchpad table` for intermediary results from `timesTwo`. This table makes `reduce` easier to implement and debug.\n2. `sums table` for final results\n\n### Step 1 – the API\n\nYou first need to get data into the system. We use a [Serverless REST API](/serverless-rest-api).\n\n```yaml\n# serverless.yml\nfunctions:\n  sumArray:\n    handler: dist/sumArray.handler\n    events:\n      - http:\n          path: sumArray\n          method: POST\n          cors: true\n    environment:\n      timesTwoQueueURL:\n        Ref: TimesTwoQueue\n```\n\n`sumArray` is an endpoint that accepts POST requests and pipes them into the `sumArray.handler` function. We set the URL for our `TimesTwoQueue` as an environment variable.\n\nDefine the queue like this:\n\n```yaml\n# serverless.yml\nresources:\n  Resources:\n    TimesTwoQueue:\n      Type: \"AWS::SQS::Queue\"\n      Properties:\n        QueueName: \"TimesTwoQueue-${self:provider.stage}\"\n        VisibilityTimeout: 60\n```\n\nIt's an SQS queue postfixed with the current stage, which helps us split between production and development.\n\nThe [`VisibilityTimeout`](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html) says that when your Lambda accepts a message, it has 60 seconds to process. After that, SQS assumes you never received the message and tries again.\n\nDistributed systems are fun like that.\n\nThe `sumArray.ts` Lambda that accepts requests looks like this:\n\n```typescript\n// src/sumArray.ts\n\nexport const handler = async (event: APIGatewayEvent): Promise<APIResponse> => {\n  const arrayId = uuidv4()\n\n  if (!event.body) {\n    return response(400, {\n      status: \"error\",\n      error: \"Provide a JSON body\",\n    })\n  }\n\n  const array: number[] = JSON.parse(event.body)\n\n  // split array into elements\n  // trigger timesTwo lambda for each entry\n  for (let packetValue of array) {\n    await sendSQSMessage(process.env.timesTwoQueueURL!, {\n      arrayId,\n      packetId: uuidv4(),\n      packetValue,\n      arrayLength: array.length,\n      packetContains: 1,\n    })\n  }\n\n  return response(200, {\n    status: \"success\",\n    array,\n    arrayId,\n  })\n}\n```\n\nGet API request, create an `arrayId`, parse JSON body, iterate over the input, return success and the new `arrayId`. Consumers can later use this ID identify their result.\n\n### Triggering the next step\n\nWe turn each element of the input array into a processing packet and send it to our SQS queue as a message.\n\n```typescript\n// src/sumArray.ts\n\nfor (let packetValue of array) {\n  await sendSQSMessage(process.env.timesTwoQueueURL!, {\n    arrayId,\n    packetId: uuidv4(),\n    packetValue,\n    arrayLength: array.length,\n    packetContains: 1,\n  })\n}\n```\n\nEach packet contains:\n\n- `arrayId` to identify which input it belongs to\n- `packetId` to identify the packet itself\n- `packetValue` as the value. You could use this to store entire JSON blobs.\n- `arrayLength` to help `reduce` know how many packets to expect\n- `packetContains` to help `reduce` know when it's done\n\nMost properties are metadata to help our pipeline. `packetValue` is the data we're processing.\n\n[`sendSQSMessage`](https://github.com/Swizec/serverlesshandbook.dev/blob/master/examples/serverless-data-pipeline-example/src/utils.ts#L11) is a helper method that sends an SQS message using the AWS SDK.\n\n### Step 2 – the map\n\nOur map uses the `timesTwo` lambda and handles each packet in isolation.\n\n```yaml\n# serverless.yml\nfunctions:\n  timesTwo:\n    handler: dist/timesTwo.handler\n    events:\n      - sqs:\n          arn:\n            Fn::GetAtt:\n              - TimesTwoQueue\n              - Arn\n          batchSize: 1\n    environment:\n      reduceQueueURL:\n        Ref: ReduceQueue\n```\n\n`timesTwo` handles events from the `TimesTwoQueue` using the `timesTwo.handler` method. It gets the `reduceQueueURL` as an environment variable to trigger the next step.\n\nAWS and SQS call our lambda and keep retrying when something goes wrong. Perfect to let you re-deploy when there's a bug ✌️\n\nThe lambda looks like this:\n\n```typescript\n// src/timesTwo.ts\n\nexport const handler = async (event: SQSEvent) => {\n  // grab messages from queue\n  // depending on batchSize there could be multiple\n  let packets: Packet[] = event.Records.map((record: SQSRecord) =>\n    JSON.parse(record.body)\n  )\n\n  // iterate packets and multiply by 2\n  // this would be a more expensive operation usually\n  packets = packets.map((packet) => ({\n    ...packet,\n    packetValue: packet.packetValue * 2,\n  }))\n\n  // store each result in scratchpad table\n  // in theory it's enough to put them on the queue\n  // an intermediary table makes the reduce step easier to implement\n  await Promise.all(\n    packets.map((packet) =>\n      db.updateItem({\n        TableName: process.env.SCRATCHPAD_TABLE!,\n        Key: { arrayId: packet.arrayId, packetId: packet.packetId },\n        UpdateExpression:\n          \"SET packetValue = :packetValue, arrayLength = :arrayLength, packetContains = :packetContains\",\n        ExpressionAttributeValues: {\n          \":packetValue\": packet.packetValue,\n          \":arrayLength\": packet.arrayLength,\n          \":packetContains\": packet.packetContains,\n        },\n      })\n    )\n  )\n\n  // trigger next step in calculation\n  const uniqueArrayIds = Array.from(\n    new Set(packets.map((packet) => packet.arrayId))\n  )\n\n  await Promise.all(\n    uniqueArrayIds.map((arrayId) =>\n      sendSQSMessage(process.env.reduceQueueURL!, arrayId)\n    )\n  )\n\n  return true\n}\n```\n\nAccept an event from SQS, parse JSON body, do the work, store intermediary results, trigger `reduce` step for each input.\n\nSQS might call your lambda with multiple messages depending on your `batchSize` config. This helps you optimize cost and find the right balance between the number of executions and execution time.\n\n### Trigger the next step\n\nTriggering the next step happens in 2 parts\n\n1. store intermediary result\n2. trigger next lambda\n\nStoring intermediary results makes implementing the next lambda easier. We're building a faux queue on top of DynamoDB. That's okay because it makes the system easier to debug.\n\nSomething went wrong? Check intermediary table, see what's up.\n\nWe iterate over results, fire a DynamoDB `updateItem` query, and await the queries.\n\n```typescript\n// src/timesTwo.ts\n\n// store each result in scratchpad table\n// in theory it's enough to put them on the queue\n// an intermediary table makes the reduce step easier to implement\nawait Promise.all(\n  packets.map((packet) =>\n    db.updateItem({\n      TableName: process.env.SCRATCHPAD_TABLE!,\n      Key: { arrayId: packet.arrayId, packetId: packet.packetId },\n      UpdateExpression:\n        \"SET packetValue = :packetValue, arrayLength = :arrayLength, packetContains = :packetContains\",\n      ExpressionAttributeValues: {\n        \":packetValue\": packet.packetValue,\n        \":arrayLength\": packet.arrayLength,\n        \":packetContains\": packet.packetContains,\n      },\n    })\n  )\n)\n```\n\nEach entry in this table is uniquely identified with a combination of `arrayId` and `packetId`.\n\n_Triggering_ the next step happens in another loop.\n\n```typescript\n// src/timesTwo.ts\n\n// trigger next step in calculation\nconst uniqueArrayIds = Array.from(\n  new Set(packets.map((packet) => packet.arrayId))\n)\n\nawait Promise.all(\n  uniqueArrayIds.map((arrayId) =>\n    sendSQSMessage(process.env.reduceQueueURL!, arrayId)\n  )\n)\n```\n\nWe use an ES6 Set to get a list of unique array ids from our input message. You never know what gets jumbled up on the queue and you might receive multiple inputs in parallel.\n\nDistributed systems are fun :)\n\nFor each unique input, we trigger a reduce lambda via `ReduceQueue`. A single `reduce` stream makes this example easier. You should aim for parallelism in production.\n\n### Step 3 – reduce\n\nCombining intermediary steps into the final result is the most complex part of our example.\n\nThe simplest approach is to take the entire input and combine in 1 step. With large datasets this becomes impossible. Especially if combining elements is a big operation.\n\nYou could combine 2 elements at a time and run the reduce step in parallel.\n\nFine-tuning for number of invocations and speed of execution will result in an `N` somewhere between `2` and `All`. Optimal numbers depend on what you're doing.\n\nOur reduce function uses the `scratchpad table` as a queue:\n\n1. Take 2 elements\n2. Combine\n3. Write the new element\n4. Delete the 2 originals\n\nLike this:\n\n![How rows in the scratchpad table get combined](../../images/reduce-table.png)\n\nAt each invocation we take 2 packets:\n\n```\n{\n\tarrayId: // ...\n\tpacketId: // ...\n\tpacketValue: 2,\n\tpacketContains: 1,\n\tarrayLength: 10\n}\n\n{\n\tarrayId: // ...\n\tpacketId: // ...\n\tpacketValue: 4,\n\tpacketContains: 1,\n\tarrayLength: 10\n}\n```\n\nAnd combine them into a new packet\n\n```\n// +\n{\n\tarrayId: // ...\n\tpacketId: // ...\n\tpacketValue: 6\n\tpacketContains: 2,\n\tarrayLength: 10\n}\n```\n\nWhen the count of included packets – `packetContains` – matches the total array length – `arrayLength` – we know this is the final result and write it into the results table.\n\nLike I said, this is the complicated part.\n\n### The reduce step code\n\nIn code, the reduce step starts like any other lambda:\n\n```typescript\n// src/reduce.ts\nexport const handler = async (event: SQSEvent) => {\n  // grab messages from queue\n  // depending on batchSize there could be multiple\n  let arrayIds: string[] = event.Records.map((record: SQSRecord) =>\n    JSON.parse(record.body)\n  )\n\n  // process each ID from batch\n  await Promise.all(arrayIds.map(reduceArray))\n}\n```\n\nGrab `arrayId`s from the SQS event and wait until every `reduceArray` call is done.\n\nThen the `reduceArray` function does its job:\n\n```typescript\n// src/reduce.ts\n\nasync function reduceArray(arrayId: string) {\n  // grab 2 entries from scratchpad table\n  // IRL you'd grab as many as you can cost-effectively process in execution\n  // depends what you're doing\n  const packets = await readPackets(arrayId)\n\n  if (packets.length > 0) {\n    // sum packets together\n    const sum = packets.reduce(\n      (sum: number, packet: Packet) => sum + packet.packetValue,\n      0\n    )\n\n    // add the new item sum to scratchpad table\n    // we do this first so we don't delete rows if it fails\n    const newPacket = {\n      arrayId,\n      packetId: uuidv4(),\n      arrayLength: packets[0].arrayLength,\n      packetValue: sum,\n      packetContains: packets.reduce(\n        (count: number, packet: Packet) => count + packet.packetContains,\n        0\n      ),\n    }\n    await db.updateItem({\n      TableName: process.env.SCRATCHPAD_TABLE!,\n      Key: {\n        arrayId,\n        packetId: uuidv4(),\n      },\n      UpdateExpression:\n        \"SET packetValue = :packetValue, arrayLength = :arrayLength, packetContains = :packetContains\",\n      ExpressionAttributeValues: {\n        \":packetValue\": newPacket.packetValue,\n        \":arrayLength\": newPacket.arrayLength,\n        \":packetContains\": newPacket.packetContains,\n      },\n    })\n\n    // delete the 2 rows we just summed\n    await cleanup(packets)\n\n    // are we done?\n    if (newPacket.packetContains >= newPacket.arrayLength) {\n      // done, save sum to final table\n      await db.updateItem({\n        TableName: process.env.SUMS_TABLE!,\n        Key: {\n          arrayId,\n        },\n        UpdateExpression: \"SET resultSum = :resultSum\",\n        ExpressionAttributeValues: {\n          \":resultSum\": sum,\n        },\n      })\n    } else {\n      // not done, trigger another reduce step\n      await sendSQSMessage(process.env.reduceQueueURL!, arrayId)\n    }\n  }\n}\n```\n\nGets 2 entries from the DynamoDB table[^2] and combines them into a new packet.\n\nThis packet _must_ have a new `packetId`. It's a new entry for the faux processing queue on DynamoDB.\n\nInsert that back. If it succeeds, remove the previous 2 elements using their `arrayId/packetId` combination.\n\nIf our new `packetContains` is greater than or equal to the total `arrayLength`, we know the computation is complete. Write results to the final table.\n\n## Conclusion\n\nLambda processing pipelines are a powerful tool that can process large amounts of data in near real-time. I have yet to find a way to swamp one of these in production.\n\nSlow downs happen where you have to introduce throttling to limit processing because a downstream system can't take the load.\n\n✌️\n\nNext chapter, we talk about monitoring serverless apps.\n\n[^1]: Our dataloss happened because of Postgres. Sometimes it would fail to insert a row, but wouldn't throw an error. We mitigated with various workarounds but a small loss remained. At that point we decided it was good enough.\n[^2]: We use a `Limit: 2` argument to limit how much of the table we scan through. Don't need more than 2, don't read more than 2. Keeps everything snappy\n"
  },
  {
    "path": "src/pages/robust-backend-design/index.mdx",
    "content": "---\ntitle: \"Robust backend design\"\ndescription: \"A fundamental paradox means your backend can never be perfect. How do you build code that works?\"\nimage: \"./img/robust-backend-design.png\"\n---\n\n# Robust backend design\n\n![](../../images/chapter_headers/robust-backend-design.svg)\n\nImagine you're a Roman general leading a vast and powerful army. You're about to attack a city.\n\nBut you can't do it alone.\n\nYour buddy with another vast and powerful army hides behind a hill on the other side. You need their help to win.\n\nAttack together and win. Attack alone and die.\n\nHow do you ensure a joint attack?\n\n![Two generals problem](../../images/two-generals.png)\n\nSmoke signals would reveal your plan to the city. It's too far to shout and phones are 2000 years in the future.\n\nA messenger is your best bet. Run to the other army, deliver the message, come back with confirmation.\n\nUnless they're caught. 🤔\n\nThe messenger could fail to deliver your message. Or get caught on their way back. You'll never know.\n\nSend more messengers until one makes it back? How does your friend know that any messenger made it back? Nobody wants to attack alone.\n\nThis puzzle is known as the [Two Generals' Problem](https://en.wikipedia.org/wiki/Two_Generals%27_Problem). **There is no solution**.\n\nNo algorithm guarantees 100% certainty over a lossy medium. Best you can do is *\"Pretty sure.\"*\n\nAnd that's why distributed systems are hard.\n\nYou cannot have 100% reliability. As soon as servers talk to each other, you're doomed to probabilities.\n\nServerless systems are always distributed. 😅\n\n## Build a robust backend\n\n> A robust backend keeps working in the face of failure.\n\nAs we mentioned in the [Architecture Principles chapter](https://serverlesshandbook.dev/serverless-architecture-principles), your backend follows 3 principles:\n\n1. Everything can and will fail\n2. Your system should keep working\n3. Failures should be easy to fix\n\nYou get there with a combination of error recovery, error isolation, and knowing when your system needs help.\n\nThe strategies mentioned in [Architecture Principles](https://serverlesshandbook.dev/serverless-architecture-principles) were:\n\n- isolate errors\n- retry until success\n- make operations replayable\n- be debuggable\n- remove bad requests\n- alert the engineer when something's wrong\n- control your flow\n\nThis chapter talks about how.\n\n## Isolate errors\n\n[In March 2017, Amazon S3 went down](https://www.theverge.com/2017/3/2/14792442/amazon-s3-outage-cause-typo-internet-server) and took with it half the internet. Root cause was a typo.\n\nAWS Engineers were testing what happens when a few servers go offline. A typo took out too many and the rest got overwhelmed. They started failing one by one.\n\nSoon the whole system was down.\n\nAnd because AWS relies on S3 to store files ... much of AWS went down. And because half the internet runs on AWS ... it went down.\n\nAWS couldn't even update their status dashboard because error icons live on S3.\n\n![](giphy:eesh)\n\nTo isolate errors you have to reduce inter-dependency. Always think: *\"What can I do to make moving pieces less dependent on each other?\"*\n\nIn your car, the brakes keep working even if your brake lights go out. The systems work together, but independently.\n\nInter-dependency can be subtle and hard to spot. The specifics are different each time.\n\nHere are 3 rules:\n\n1. Give each operation a single responsibility\n2. Do the whole operation in one atomic go\n3. Avoid coupling\n\nServerless functions are optimized for this approach by default. They encourage you to keep code light and isolated 🤘\n\n<div id=\"lock\" />\n\n### Give each operation a single responsibility\n\nThink of serverless code like a function. How big are your functions? How much do they do?\n\nSay you were building a basic math module. Would you write a function that performs plus *and* minus?\n\n```typescript\nfunction doMath(a: number, b: number, op: \"plus\" | \"minus\") {\n  if (op === \"plus\") {\n    return a + b\n  } else {\n    return a - b\n  }\n}\n```\n\nThat looks odd to me. Plus and minus are distinct operations.\n\nYou'd write 2 functions instead:\n\n```typescript\nfunction plus(a: number, b: number) {\n  return a + b\n}\n\nfunction minus(a: number, b: number) {\n  return a - b\n}\n```\n\nThat's easier to work with.\n\nSplitting your code into small operations is an art. Big functions are hard to debug and too many small functions are hard to understand.\n\nMy rule is that when I use \"and\" to describe a function, I need a new function.\n\n### Do the whole operation in one atomic go\n\nAtomic operations guarantee correctness. Perform an action start to finish without distraction.\n\nSay you're taking an important memory medicine.\n\nYou grab the pill bottle, take out a pill, put it on your desk, get an email and start reading. You elbow the pill off your desk.\n\n10 minutes later you look down and there's no pill. Did you take it?\n\n![](giphy:hmmm)\n\nSame thing happens when your lambdas do too much. Your code can get distracted, halfway failures happen, and you're left with a corrupt state.\n\nRecovering from corrupt state is hard.\n\nInstead, try to avoid distractions. Keep operations small, go start to finish, delegate. Let another process read email. And [clean up when there's an error](<https://en.wikipedia.org/wiki/Atomicity_(database_systems)>).\n\n### Avoid coupling\n\nWith atomic operations and delegating heavy work to other functions, you're primed for another mistake: Direct dependency.\n\nLike this:\n\n```javascript\nfunction myLambda() {\n\t// read from db\n\t// prep the thing\n\n\tawait anotherLambda(data)\n\n\t// save the stuff\n}\n```\n\nYou're coupling your `myLambda` function to your `anotherLambda` function. If `anotherLambda` fails, the whole process fails.\n\nTry decoupling these with a queue. Turn it into a process. `myLambda` does a little, pushes to the queue, `anotherLambda` does the rest.\n\n![Decoupled operations through a queue](../../images/coupled_decoupled.png)\n\nYou can see this principle in action in the [Lambda Pipelines for distributed data processing](http://serverlesshandbook.dev/lambda-pipelines) chapter.\n\n## Retry until success\n\nRetrying requests is built into the serverless model.\n\nAWS [retries every lambda invocation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-retries.html), if the call fails. The number of retries depends on who's calling.\n\nAPI Gateway is proxying requests from users and that makes retries harder than an SQS queue which has all the time in the world.\n\nRetries happen for two reasons:\n\n1. Lambda never got the message\n2. Lambda failed to process the message\n\n#1 is out of your control. Two generals problem struck between request and your code. 💩\n\n#2 means you should always throw an error when something goes wrong. Do not pretend.\n\nBoth SQS – Simple Queue Service – and SNS – Simple Notification Service – support retries out of the box. They're the most common ways AWS services communicate.\n\nDetails on how each implements retries differ. You can read more about [How SNS works](https://docs.aws.amazon.com/sns/latest/dg/sns-message-delivery-retries.html) and [How SQS works](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-basic-architecture.html) in AWS docs.\n\nBoth follow this pattern:\n\n1. Message accepted into SQS / SNS\n2. Message stored in multiple locations\n3. Message sent to your lambda\n4. Wait for lambda to process or fail\n5. If processed, remove message\n6. If failed, retry ... sometimes thousands of times\n\nWait to delete the message until after confirmation. You might lose data otherwise.\n\nKeep this in mind: never mark something as processed until you know for sure ✌️\n\n### Build replayable operations\n\nYour code can retry for any reason. Make sure that's not a problem.\n\nFollow this 4 step algorithm:\n\n1. Verify work needs doing\n2. Do the work\n3. Mark work as done\n4. Verify marking it done worked\n\nTwo Generals Problem may strike between you and your database 😉\n\nIn pseudocode, functions follow this pattern:\n\n```javascript\nfunction processMessage(messageId) {\n  let message = db.get(messageId)\n\n  if (!message.processed) {\n    try {\n      doTheWork(message)\n    } catch (error) {\n      throw error\n    }\n    message.processed = true\n\n    db.save(message)\n\n    if (db.get(messageId).processed) {\n      return success\n    } else {\n      throw \"Processing failed\"\n    }\n  }\n\n  return success\n}\n```\n\nThis guards against all failure modes:\n\n1. Processing retried, but wasn't needed\n2. Work failed\n3. Saving work failed\n\nMake sure `doTheWork` throws an error, if it fails to save. Common cause of spooky dataloss. ✌️\n\n## Be debuggable\n\n![](giphy:sherlock_holmes)\n\nDebugging distributed systems is hard. More art than science.\n\nYou'll need to know or learn your system inside-out. Tease it apart bit by bit.\n\nKeeping your code re-runnable helps. Keeping your data stored helps. Having easy access to all this helps.\n\nWhen debugging distributed systems I like to follow this approach:\n\n1. Look at the data\n2. Find which step of the process failed\n3. Get the data that failed\n4. Run the step that failed\n5. Look at logs\n\nYou can use a debugger to step through your code locally. With a unit test using production data. But it's not the same as a full production environment.\n\nIf local debugging fails, add logs. Many logs. Run in production, see what happens.\n\nBeing debuggable therefore means:\n\n1. Safely replayable operations\n2. Keep intermediate data long enough\n3. Manually executable with specific requests\n4. Identifiable and traceable requests (AWS adds requestId to every log)\n5. Locally executable for unit testing\n\n## Remove bad requests\n\nRequests can include a poison pill – a piece of bad data you can never process. They might swamp your system with infinite retries.\n\nSay you want to process requests in sequence.\n\n9 requests go great, the 10th is a poison pill. Your code gets stuck trying and retrying for days.\n\nMeanwhile users 11 to 11,000 storm your email crying that the service is down. But it's not down, it's stuck.\n\n[Dead letter queues](https://en.wikipedia.org/wiki/Dead_letter_queue) can help. They hold bad messages until you have time to debug.\n\nEach queue-facing Lambda gets two queues:\n\n1. The trigger queue\n2. A dead letter queue for bad requests\n\nLike this in `serverless.yml`:\n\n```yaml\n# serverless.yml\n\nfunctions:\n    worker:\n        handler: dist/lambdas/worker.handler\n        events:\n\t\t        # triggering from SQS events\n            - sqs:\n                  arn:\n                      Fn::GetAtt:\n                          - WorkerQueue\n                          - Arn\n                  batchSize: 1\n\nresources:\n\tResources:\n        WorkerQueue:\n            Type: \"AWS::SQS::Queue\"\n            Properties:\n                QueueName: \"WorkerQueue-${self:provider.stage}\"\n                # send to deadletter after 10 retries\n                RedrivePolicy:\n                    deadLetterTargetArn:\n                        Fn::GetAtt:\n                            - WorkerDLQueue\n                            - Arn\n                    maxReceiveCount: 10\n        WorkerDLQueue:\n            Type: \"AWS::SQS::Queue\"\n            Properties:\n                QueueName: \"WorkerDLQueue-${self:provider.stage}\"\n                # keep messages for a long time to help debug\n                MessageRetentionPeriod: 1209600 # 14 days\n```\n\nA worker lambda runs from an SQS queue. When it fails, messages are retried.\n\nNow all you need is an alarm on dead letter queue size to say *\"Hey something's wrong, you should check\"*.\n\nBug in your code? Fix the bug, re-run worker from dead letter queue. No messages are lost.\n\nBad messages? Drop 'em like it's hot.\n\n## Alert an engineer\n\nThe challenge with serverless systems is that you can't see what's going on. And you're not sitting there staring at logs.\n\n![](giphy:the_matrix_looking_at_logs)\n\nYou need a monitoring system. A way to keep tabs on your services and send alerts. Email for small problems, slack for big problems, text message for critical problems.\n\nThat's what works for me.\n\nAWS has basic monitoring built-in, Datadog is great for more control. More on monitoring in the [Monitoring serverless apps chapter](https://serverlesshandbook.dev/serverless-monitoring)\n\n## Control your flow\n\nControl your flow means looking at the performance of your system as a whole.\n\nWriting fast code is great, but if your speedy lambda feeds into a slow lambda, you're gonna have a bad day. Work piles up, systems stop, customers complain.\n\nYou want to ensure a smooth flow through the whole system.\n\nComputer science talks about this through [Queuing theory](https://en.wikipedia.org/wiki/Queueing_theory) and [stochastic modeling](https://en.wikipedia.org/wiki/Stochastic_process). Business folk talk about [Theory of constraints](https://en.wikipedia.org/wiki/Theory_of_constraints).\n\nIt's a huge field :)\n\nWe talk more about flow in the [Serverless performance](https://serverlesshandbook.dev/serverless-performance) chapter.\n\n## Conclusion\n\nIn conclusion, a distributed system is never 100% reliable. You can make it better with small replayable operations, keeping code debuggable, and removing bad requests.\n\nNext chapter we look at where to store your data.\n"
  },
  {
    "path": "src/pages/serverless-architecture-principles/index.mdx",
    "content": "---\ntitle: \"Architecture principles\"\ndescription: \"Learn how to design a serverless system that keeps working in the face of errors\"\nimage: \"./img/architecture-principles.png\"\n---\n\n# Architecture principles\n\n![](../../images/chapter_headers/architecture-principles.svg)\n\nWhat do you do when 3% of requests fail?\n\nThat was my reality one painful day in September. Deploy new feature, pat yourself on the back, go to lunch. A big feature – done. Nothing can touch you. 💪\n\nDing.\n\n*A Slack message? Who's working through lunch this time ...*\n\nDing. Ding.\n\n*Weird, that's a lot of messages for lunchtime ...*\n\nBZZZ\n\n> Everything is on fire. High API error rate 🔥<br/>\n> ~ an alarm SMS\n\nOur system sent alarms to Slack. Serious alarms to SMS.\n\nThe *high API error rate* was the biggest alarm. A catch-all that triggered when you can't be sure the more specific alarms even work.\n\nBack to my desk, my heart sank: 3% of all API requests were failing. Reasons unknown.\n\nEvery user action, every background process on the web, on iOS *and* on Android, every time you opened the site or accessed the app 👉 3% chance of failure. At our usual 10 requests per second, that's 18 errors every minute!\n\n![](giphy:oh-crap)\n\nWanna know the best part?\n\nNobody noticed. ✌️\n\nI knew something was wrong because of that SMS. The system kept hobbling along. Slower, in pain, but getting the job done.\n\nHow can a system with 18 failures per minute keep working?\n\n## Everything fails\n\n*The* design principle behind every backend architecture states:\n\n1. Everything can and will fail\n2. Your system should work anyway\n3. Make failures easy to fix\n\n<div id=\"lock\" />\n\nIn 2011 Netflix forced engineers to think about this with the [Chaos Monkey](https://en.wikipedia.org/wiki/Chaos_engineering). They wanted to *\"move from a development model that assumed no breakdowns to a model where breakdowns were considered to be inevitable\"*.\n\nThe Chaos Monkey makes that happen by introducing random failures to production environments. Servers down, databases unresponsive, message queues failing. \n\nReality is going to be your chaos monkey. Plan for it. 😉\n\n## How statistics play against you\n\nAt scale, you're playing against statistics. A one in a million error rate, at 10 requests per second, means an error every day.\n\nNot a big deal, but if that error happens in your payment flow and you double charge a user ... they'll care.\n\nWhat do statistics mean for you and your architecture? \n\n[AWS Lambda](https://aws.amazon.com/lambda/sla/) guarantees a 99.95% monthly uptime percentage. That's your base error rate.\n\n**Even if you write the most perfect code, you can't have a lower error rate than your platform.**\n\nWhat does *99.95% monthly uptime percentage* mean in practice?\n\n> “Monthly Uptime Percentage” for a given AWS region is calculated as the average of the Availability for all 5-minute intervals in a monthly billing cycle. Monthly Uptime Percentage measurements exclude downtime resulting directly or indirectly from any Lambda SLA Exclusion.\n> \n> “Availability” is calculated for each 5-minute interval as the percentage of Requests processed by Lambda that do not fail with Errors and relate solely to the provisioned Lambda functions. If you did not make any Requests in a given 5-minute interval, that interval is assumed to be 100% available.\n\n![](giphy:head-scratch)\n\n### Let's run the numbers\n\nThere are `24*60/5 = 288` 5-minute intervals in a day. An average of 99.95% means that some intervals have lower availability, some higher. None go higher than 100%.\n\nThe hidden message is that you're going to see bursts of failures. A bad 5-minute period in a sea of 100% green. Outliers pull the average down.\n\nLet's assume an even error rate to simplify the math.\n\nAt 1 request per 5 minutes, you get `288 * 0.9995 = 287.86` successful requests. Round up to 288. All good.\n\nBut at 1 per minute, you're down to `1439` successful requests. An error per day.\n\nIt gets worse from there.\n\nI once built a large system running at a 0.092% failure rate. Lower than the *error* rate of the platform because of the architecture.\n\nPretty good eh?\n\n*1069 errors per day.*\n\n![](giphy:oof)\n\n*PS: failure rate means the system never manages to process a request, error rate means individual, possibly recoverable, errors along the way*\n\n## How to design a resilient architecture\n\n[Resilience](https://en.wikipedia.org/wiki/Resilience_(engineering_and_construction)) is key. Designing your system to withstand shocks and errors. To work as a whole when individual pieces fail.\n\nYour goal is to:\n\n- **isolate errors** when an error happens, confine it to the smallest area possible without damaging the rest of your system\n- **make operations replayable** triggering the same operation multiple times must produce the same result\n- **retry until success** errors are often transient and go away when you try again\n- **make your system debuggable** store enough information about your actions so that you can look back later\n- **remove unprocessable requests** sometimes requests can't be processed. Make sure these don't kill your system or get in the way of valid requests\n- **alert the engineer when something is wrong** errors can be part of normal operations. When there's too many, make your system cry out in pain and tell you where it hurts\n\nYou can achieve all that with a few steps.\n\n### Small, isolated, replayable operations\n\nThink of your task as a pipeline. \n\nA request comes in. You do X, then you do Y, then Z, then the result comes out. The request is your input, the result is your output.\n\nLike Henry Ford's famous assembly line 👉 steel comes in the factory on one end, cars leave on the other. \n\nFor max resilience, make each operation follow this algorithm:\n\n- get request\n- check if request already processed\n- if processed, finish\n- if not, do your thing\n- trigger the next step\n- mark request as processed\n\n**Every request needs an identifier.** Easiest if it's a unique `id` of a database row. \n\nHaving a way to answer *\"Did I process this yet?\"* gives you replayability. Call the same action with the same request twice and nothing happens.\n\nEasiest way to achieve this, is a lookup table. I like to put a `processed` flag in each database row. Lets you pass a reference to the row between actions. Each does its thing and changes the flag.\n\n**Each action does one thing and one thing only.**\n\nBecause each action performs one step in the process, you can look at the `processed` flags to see what's up.\n\nDebugging becomes easier. You can test steps in the process on their own. Like a function call.\n\n**Isolation comes from decoupling** \n\nIsolation ensures that an error in one action doesn't impact others. *That* action fails and the pipeline might get backed up, but other actions keep working fine.\n\nAnd because you have replayability, you can retry until your action succeeds. ✌️\n\n### Retry until success\n\nFire an action, get success or fail. If success, remove message from the queue and move on. If it fails, put message back on queue and try again.\n\nErrors can be temporary. Engineers will fix the bug. \n\nIf it's a hardware problem, you can wait until your function runs on a different physical machine. Yay serverless.\n\nYou can keep retrying until success because actions are safely replayable.\n\nTo be safe, I like to have a **cleanup worker** that goes through the database every few hours and checks for rows that failed to process and stopped retrying. \n\nCatches errors in the retry system itself. ✌️\n\n*PS: more on queues and how this works in [the chapter on serverless elements](/serverless-elements)*\n\n### A debuggable system is a good system\n\nSmall actions that store a *\"Yes I Did It\"* state for each piece of data are easier to debug. You can look at your database to see which step failed.\n\nReplayability lets you trigger the failed action for that data and see what happens.\n\nSince actions are functions and your messages are plain data, you can test locally. Unit tests are great, running production code on production data in your terminal, that's wow.\n\nAnd if that's not enough, add logging. A well-placed `console.log` can do wonders.\n\n## Conclusion\n\n> Build your system out of small isolated pieces that talk to each other via queues.\n\nNext chapter we dive into queues and lambdas, and talk about how to tie them together into a system."
  },
  {
    "path": "src/pages/serverless-authentication/index.mdx",
    "content": "---\ntitle: \"Serverless authentication\"\ndescription: \"Learn about user authentication and how it works\"\nimage: \"./img/dealing-with-authentication.png\"\n---\n\n# Serverless authentication\n\n![](../../images/chapter_headers/dealing-with-authentication.svg)\n\nYou've got a feature that only a few people should use. How do you keep it safe?\n\nAuthentication. \n\nIt's easy in theory: Save an identifier on the client, send with every request, check against a stored value on the server.\n\nIn practice, authentication is hard.\n\nWhere do you save the identifier? How does the client get it? What authentication scheme do you use? What goes on the server? How do you keep it secure? What if you need to run authenticated code without the user?\n\nAuthentication is a deep rabbit hole. In this chapter, we look at the core ideas and build 2 examples.\n\n## What is authentication\n\nA typical authentication system deals with everything from user identity, to access control, authorization, and keeping your system secure.\n\nIt's a big job :)\n\n**Identity** answers the *\"Who are you?\"* question. The most important aspect of authentication systems. Can be as simple as an honor-based input field.\n\n**Access control** answers the *\"Can you access this system?\"* question. You ask for proof of identity (like a password) and unlock restricted parts of your app.\n\n**Authorization** answers the *\"Which parts of the system can you use?\"* question. Two schemes are common: role-based and scope-based authorization. They specify which users can do what.\n\n**Security** underlies your authentication system. Without security, you've got nothing. \n\nTypical concerns include leaking authentication tokens, interception attacks, impersonating users, revoking access, how your data behaves, and whether you can identify a breach.\n\n### Factors of authentication\n\nProof of identity is key to good authentication.\n\n> something the user *knows*, something the user *has*, something the user *is*\n\nEach [authentication factor](https://en.wikipedia.org/wiki/Authentication#Authentication_factors) covers a different overlapping proof of identity. 2 factors is considered safe, 3 is best. Typical is 1 🙈\n\n**Knowledge factors** include hidden pass phrases like passwords, PINs, and security questions. You know the answer and verify the user knows too.\n\n**Ownership factors** include ID cards, token apps on your phone, physical tokens, and email inboxes. You ask the user to prove they have something only they should have.\n\n**Inference factors** include biometric identifiers like fingerprints, DNA, hand-written signatures, and other markers that uniquely identify a person.\n\nCredit card + PIN is 2-factor authentication. You own the card and know the PIN.\n\nUsername + password is 1-factor. You know the username and know the password.\n\nPasswordless email/sms login is 2-factor. You know the username and own the email inbox. Proof by unique link or pin.\n\n### Role-based and scope-based authorization\n\nThe 2nd job after access control is authorization. What can *this* user do?\n\nTwo flavors of authorization are common:\n\n- **role-based** authorization depends on user types. Admins vs. paid users vs. freeloaders.\n- **scope-based** authorization depends on fine-grained user properties. Enable subscriber dashboard or don't.\n\nTechnically they're the same – a user property. It's like utility vs. semantic classes in CSS. Debate until you're blue in the face, then pick what feels right :)\n\nRole-based authorization is perfect for small projects. You need admins and everyone else. Being an admin comes with inherent rights.\n\nScope-based authorization is perfect for large projects with granular needs. Yes you're an admin, but admin of what?\n\nIn practice you'll see that roles get clunky and scopes are tedious. Like my dayjob gave me permission to configure CloudFront, but not to see what I'm doing. 🙃\n\nAt an organizational level you end up with roles that act as bags of scopes. Engineers get scopes x, y, z, admins can do so and so, and users get user things.\n\n## Build your own auth\n\nLet's build a basic serverless auth designed to be used as an API. It's the best way to get a feel for what it takes.\n\nI'll share and explain the important code. You can [see the full example on GitHub](https://github.com/Swizec/serverlesshandbook.dev/tree/master/examples/serverless-auth-example). Use [this CodeSandbox app](https://codesandbox.io/s/serverless-auth-example-9ipfb) to try it out.\n\nhttps://codesandbox.io/s/serverless-auth-example-9ipfb\n\nYou can test your implementation too. Change the Lambda base URL 😊\n\n### The auth flow\n\nTraditional auth and API auth use a different medium to exchange tokens. Traditional auth uses cookies, API auth relies on [JSON Web Tokens (JWT)](https://jwt.io/) and `Authorization` headers.\n\n![Authentication flow](../../images/authentication_flow.png)\n\nThe API approach works great with modern JavaScript apps, mobile clients, and other servers.\n\n1. User sends username and password\n2. Server checks against database\n3. Returns fresh JWT token\n4. Client adds token to every future request\n5. Server checks token is valid before responding\n\nThis flow is secure because of the HTTPS encryption used at the protocol level. Without it, attackers could steal passwords and tokens by sniffing API requests.\n\nAlways use HTTPS and remember: *A JWT token on its own lets you impersonate a user*.\n\n### A note on password safety\n\nYou need to know the user's password to verify the user got it right. But *never* store a plain password.\n\nA typical approach is to use a [one-way hash](https://en.wikipedia.org/wiki/Cryptographic_hash_function). Feed password into a cryptographic function, get unique value that's impossible to reverse.\n\nSince the invention of [rainbow tables](https://en.wikipedia.org/wiki/Rainbow_table) even one-way hashing is no longer secure. You can fight that with a salt.\n\n```typescript\n// src/util.ts\n\n// Hashing your password before saving is critical\n// Hashing is one-way meaning you can never guess the password\n// Adding a salt and the username guards against common passwords\nexport function hashPassword(username: string, password: string) {\n  return sha256(\n    `${password}${process.env.SALT}${username}${password}`\n  ).toString()\n}\n```\n\nWithout a salt, the string `password` turns into the same hash for every app. Precomputed rainbow tables work like magic.\n\nWith a salt, the string `password` hashes uniquely to *your* app. Attackers need new rainbow tables, *if they can find the salt.*\n\nAdd the username and each hash is unique to your app *and* the user. Creating new rainbow tables for every user is not worth it.\n\nThat's when guessing becomes the easier approach. You can fight guessing with limits and timeouts on your login API.\n\n### Environment variables\n\nWe need 2 variables to build an auth system:\n\n- a unique `SALT` for password hashing\n- a unique `JWT_SECRET` for signing JWT tokens\n\nWe define them in `serverless.yml` to keep things simple. Use [proper secrets handling](/handling-secrets) for production.\n\n```yaml\n# serverless.yml\nservice: serverless-auth-example\n\nprovider:\n  # ...\n  environment:\n    SALT: someRandomSecretString_pleaseUseProperSecrets:)\n    JWT_SECRET: useRealSecretsManagementPlease\n```\n\nNever share these with anyone. `JWT_SECRET` is all an attacker needs to impersonate a user.\n\n### `auth.login` function\n\nUsers need to be able to login – send an API request with their username and password to get a JWT token. We'll keep it similar to the [REST API chapter](/serverless-rest-api).\n\n```yaml\n# serverless.yml\n\nfunctions:\n  login:\n    handler: dist/auth.login\n    events:\n      - http:\n          path: login\n          method: POST\n          cors: true\n```\n\nWe create a Lambda function called `login` that accepts POST requests and lives in the `auth` file.\n\n```typescript\n// dist/auth.ts\n\n// Logs you in based on username/password combo\n// Creates user on first login\nexport const login = async (event: APIGatewayEvent) => {\n  const { username, password } = JSON.parse(event.body || \"{}\")\n\n  // respond with error if username/password undefined\n\n  // find user in database\n  let user = await findUser(username)\n\n  if (!user) {\n    // user was not found, create\n    user = await createUser(username, password)\n  } else {\n    // check credentials\n    if (hashPassword(username, password) !== user.password) {\n      // 🚨\n      return response(401, {\n        status: \"error\",\n        error: \"Bad username/password combination\",\n      })\n    }\n  }\n\n  // user was created or has valid credentials\n  const token = jwt.sign(omit(user, \"password\"), process.env.JWT_SECRET!)\n\n  return response(200, {\n    user: omit(user, \"password\"),\n    token,\n  })\n}\n```\n\nWe grab `username` and `password` from request body and look for the user. `findUser` runs a database query, in our case a DynamoDB `getItem`.\n\nIf the user wasn't found, we create one and make sure to `hashPassword()` before saving.\n\nIf the user was found, we verify credentials by hashing the password and comparing with the stored value. We know passwords match when the `hashPassword()` method creates the same hash.\n\n> This means you can never change your `hashPassword()` method unless you force users to reset their password.\n\nThen we sign a JWT token with our secret and send it back. Make sure you don't send sensitive data like passwords to the client. Even hashed.\n\n```typescript\n\t// user was created or has valid credentials\n  const token = jwt.sign(omit(user, \"password\"), process.env.JWT_SECRET!)\n\n  return response(200, {\n    user: omit(user, \"password\"),\n    token,\n  })\n```\n\nWe're using the [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken) library to create the token.\n\nYou can [try it in the CodeSandbox](https://codesandbox.io/s/serverless-auth-example-9ipfb). Pick a username, add a password, see it work. Then try with a different password.\n\n### `auth.verify` function\n\nFor authentication to work across page reloads, you have to store the JWT token. These can expire or get revoked by the server.\n\nClients use the `verify` API to validate a session every time they initialize. A page reload on the web.\n\nWhen you know the session is valid, you treat the user as logged in. Ask for a username/password otherwise.\n\n`serverless.yml` definition is almost the same:\n\n```yaml\n# serverless.yml\n\nfunctions:\n\t# ...\n  verify:\n    handler: dist/auth.verify\n    events:\n      - http:\n          path: verify\n          method: POST\n          cors: true\n```\n\nFunction called `verify` that accepts POST requests and lives in `auth.ts`.\n\n```typescript\n// src/auth.ts\n\n// Verifies you have a valid JWT token\nexport const verify = async (event: APIGatewayEvent) => {\n  const { token } = JSON.parse(event.body || \"{}\")\n\n  // respond with error if token undefined\n\n  try {\n    jwt.verify(token, process.env.JWT_SECRET!)\n    return response(200, { status: \"valid\" })\n  } catch (err) {\n    return response(401, err)\n  }\n}\n```\n\nVerifying a JWT token with [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken) throws an error for bad tokens. Anything from a bad secret, to tampering, and token expiration.\n\n### `private.hello` function\n\nThis is where it gets fun – verifying authentication for private APIs.\n\nWe make an API that says hello to the user.\n\n```yaml\n# serverless.yml\n\nfunctions:\n  privateHello:\n    handler: dist/private.hello\n    events:\n      - http:\n          path: private\n          method: GET\n          cors: true\n```\n\nFunction accepts GET requests and lives in the `private.ts` file. It looks like this:\n\n```typescript\n// src/private.ts\n\nexport async function hello(event: APIGatewayEvent) {\n  // returns JWT token payload\n  const user = checkAuth(event) as User\n\n  if (user) {\n    return response(200, {\n      message: `Hello ${user.username}`,\n    })\n  } else {\n    return response(401, {\n      status: \"error\",\n      error: \"This is a private resource\",\n    })\n  }\n}\n```\n\nThe `checkAuth` method takes our request, verifies its JWT token, and returns the payload. A user in our case.\n\nIf user is authorized, we say hello, otherwise return an error.\n\n`checkAuth` is where we read a token from the `Authorization` header and verify it looks good.\n\n```typescript\n// src/util.ts\n\n// Used to verify a request is authenticated\nexport function checkAuth(event: APIGatewayEvent): boolean | User {\n  const bearer = event.headers[\"Authorization\"]\n\n  if (bearer) {\n    try {\n      const decoded = jwt.verify(\n        // Bearer prefix from Authorization header\n        bearer.replace(/^Bearer /, \"\"),\n        process.env.JWT_SECRET!\n      )\n\n      // We saved user info in the token\n      return decoded as User\n    } catch (err) {\n      return false\n    }\n  } else {\n    return false\n  }\n}\n```\n\nThe `Authorization` header holds our token – `Authorization: Bearer <token>`. If the header is empty, return `false`.\n\nThe `jwt.verify()` method verifies the token was valid. Checks that it was created with our secret, wasn't tampered with, and hasn't expired.\n\n`verify` decodes the token for us, which means we can see the user's username without a database query. 🤘\n\n## Use an auth provider\n\nAn auth provider like Auth0, Okta, AWS Cognito, Firebase Auth, and others makes your integration more complex. But *you* don't have to worry about password security and user management.\n\nAnd if a big provider gets hacked, your app is one among thousands. Feels less bad eh? 😇\n\n![Auth flow with a 3rd party provider](../../images/auth_provider_flow.png)\n\nThe key difference with a provider is the 3-way trust model. Users authenticate with a provider and send you a JWT token.\n\nBut you're not the authority.\n\nThat means a login dance between your server and the auth provider. You'll need to send your own JWT token to ask if the user's token is valid.\n\nExact integration depends on your provider of choice. I recommend following their documentation.\n\nIf performance is critical, this option is not great. You're adding API requests to every private call.\n\n### An example integration\n\nHere's what I do to create users on Auth0 when you purchase a course on Gumroad, a payments provider. It's the `provider <> lambda` part of the flow. \n\nThe course platform runs in the browser, which means the `user <> lambda` connection wasn't necessary. A benefit of using a provider 😊\n\nGumroad sends a POST request to AWS Lambda on every purchase. It runs this function:\n\n```typescript\nexport const pingHandler = async (\n    event: APIGatewayEvent\n): Promise<APIResponse> => {\n    const ping: GumroadPing = qs.parse(event.body!) as any;\n\n    if (ping.product_permalink in PRODUCTS) {\n        // create user from Gumroad data\n        const user = await upsertUser(ping);\n\n        if (user) {\n            // initialize Auth0 server client\n            const auth0 = await getAuth0Client();\n            const roleId = PRODUCTS[ping.product_permalink];\n\n            // give access to the course\n            await auth0.assignRolestoUser(\n                { id: user.user_id! },\n                {\n                    roles: [roleId],\n                }\n            );\n        }\n    }\n\n    return response(200, {});\n};\n```\n\nThe function parses data from Gumroad, creates a user on Auth0, and assigns the right role. I treat roles as scopes 👉 which courses can this user access?\n\n#### Auth0 Client\n\nGetting the Auth0 client means authenticating the server through a shared secret. Like a username and password.\n\n```typescript\nasync function getAuth0Client() {\n    const secrets = await auth0Tokens();\n    const auth0 = new ManagementClient({\n        domain: `${secrets.domain}.auth0.com`,\n        clientId: secrets.clientId,\n        clientSecret: secrets.clientSecret,\n        scope: \"read:users update:users create:users\",\n    });\n\n    return auth0;\n}\n```\n\n`getAuth0Tokens` talks to AWS Secrets Manager to retrieve secrets. We feed those to a new `ManagementClient`, which will use them to sign JWT tokens for future requests. \n\nAuth0 will be able to `jwt.verify()` to see that we know the right secrets.\n\nA `ManagementClient` can manages users. For authentication you'd use the `AuthenticationClient`.\n\nWe ask for as little permission as possible. If someone steals these secrets, or the code, they can't do much. \n\nIf an attacker tries to expand permissions, Auth0 config says *\"hey this client can't do that even if it asks\"*\n\n#### Create user\n\nCreating a user is a matter of calling the right methods. Look for user, create if not found.\n\n```typescript\nasync function upsertUser(purchaseData: GumroadPing) {\n    const auth0 = await getAuth0Client();\n\n    // find user\n    const users = await auth0.getUsersByEmail(purchaseData.email);\n\n    // create if not found\n    if (users.length > 0) {\n        return users[0];\n    } else {\n        return auth0.createUser({\n            // user data\n        });\n    }\n}\n```\n\n## What approach to choose?\n\nHard to say. \n\nBuilding your own is fun the first 2 or 3 times. User management and authorization is where you go ouch and think *\"I have better shit to do damn it\"*.\n\nLibraries help :)\n\nAn auth provider has more engineers to think about these problems and give you a nice experience. But integration is more complex.\n\nWhatever you do, don't build your own cryptography. ✌️\n"
  },
  {
    "path": "src/pages/serverless-chrome-puppeteer/index.mdx",
    "content": "---\ntitle: \"Serverless Chrome puppeteer\"\ndescription: \"Build browser automations with Chrome Puppeteer and AWS Lambda. Take screenshots, test websites, scrape content. Anything a browser can do ✌️\"\nimage: \"./img/serverless-chrome-puppeteer.png\"\n---\n\n# Serverless Chrome puppeteer\n\n![](../../images/chapter_headers/serverless-chrome-puppeteer.svg)\n\nSay you want to build a scraper, automate manual testing, or generate custom social cards for your website. What do you do?\n\nYou could spin up a docker container, set up headless Chrome, add Puppeteer, write a script to run it all, add a server to create an API, and ...\n\nOr you can set up Serverless Chrome with AWS Lambda. Write a bit of code, hit deploy, and get a Chrome browser running on demand.\n\nThat's what this chapter is about 🤘\n\nYou'll learn how to:\n\n- configure Chrome Puppeteer on AWS\n- build a basic scraper\n- take website screenshots\n- run it on-demand\n\nWe build a scraper that goes to [google.com](https://google.com), types in a phrase, and returns the first page of results. Then reuse the same code to return a screenshot.\n\nYou can see [full code on GitHub](https://github.com/Swizec/serverlesshandbook.dev/tree/master/examples/serverless-chrome-example)\n\n## Serverless Chrome\n\nChrome's engine ships as the open source Chromium browser. Other browsers use it and add their own UI and custom features.\n\nYou can use the engine for browser automation – scraping, testing, screenshots, etc. When you need to render a website, Chromium is your friend.\n\nThis means:\n\n- download a chrome binary\n- set up an environment that makes it happy\n- run in headless mode\n- configure processes that talk to each other via complex sockets\n\n![](giphy:ugh)\n\nOthers have solved this problem for you.\n\nRather than figure it out yourself, I recommend using [chrome-aws-lambda](https://github.com/alixaxel/chrome-aws-lambda). It's the most up-to-date package for running Serverless Chrome.\n\nHere's what you need for a Serverless Chrome setup:\n\n1. **install dependencies**\n\n```\n$ yarn add chrome-aws-lambda@3.1.1 puppeteer@3.1.0 @types/puppeteer puppeteer-core@3.1.0\n```\n\nThis installs everything you need to both run and interact with Chrome. ✌️\n\nCheck [chrome-aws-lambda/README](https://github.com/alixaxel/chrome-aws-lambda#versioning) for the latest version of Chrome Puppeteer you can use. Make sure they match.\n\n2. **configure serverless.yml**\n\n```yaml\n# serverless.yml\n\nservice: serverless-chrome-example\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: dev\n\npackage:\n  exclude:\n    - node_modules/puppeteer/.local-chromium/**\n```\n\nConfigure a new service, make it run on AWS, use latest node.\n\nThe `package` part is important. It tells Serverless _not_ to package the chromium binary with your code. AWS rejects builds of that size.\n\nYou are now ready to start running Chrome ✌️\n\n<div id=\"lock\" />\n\n### Chrome Puppeteer 101\n\n[Chrome Puppeteer](https://pptr.dev/) is a set of tools to interact with Chrome programmatically.\n\n> Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.\n\nWrite code that interacts with a website like a person would. Anything a person can do on the web, you can do with Puppeteer.\n\nCore syntax feels like jQuery, but the objects are different than what you're used to. I've found it's best not to worry about the details.\n\nHere's how you click on a link:\n\n```javascript\nconst page = await browser.newPage() // open a \"tab\"\npage.goto(\"https://example.com\") // navigates to URL\n\nconst div = await page.$(\"div#some_content\") // grab a div\nawait div.click(\"a.target_link\") // clicks link\n```\n\nAlways open a new page for every new browser context.\n\nNavigate to your URL then use jQuery-like selectors to interact with the page. You can feed selectors into `click()` and other methods, or use the `page.$` syntax to search around.\n\n## Build a scraper\n\nWeb scraping is fiddly but sounds simple in theory:\n\n- load website\n- find content\n- read content\n- return content in new format\n\nBut that doesn't generalize. Each website is different.\n\nYou adapt the core technique to each website you scrape and there's no telling when the HTML might change.\n\nYou might even find websites that actively fight against scraping. Block bots, limit access speed, obfuscate HTML, ...\n\n_Please play nice and don't unleash thousands of parallel requests onto unsuspecting websites._\n\nYou can watch me work on this project on YouTube, if you prefer video:\n\nhttps://www.youtube.com/watch?v=wRJTxahPIi4\n\nAnd you can try the final result here: [https://4tydwq78d9.execute-api.us-east-1.amazonaws.com/dev/scraper](https://4tydwq78d9.execute-api.us-east-1.amazonaws.com/dev/scraper)\n\n### 1. more dependencies\n\nStart with the `serverless.yml` and dependencies from earlier (chrome-aws-lambda and puppeteer).\n\nAdd `aws-lambda`:\n\n```\n$ yarn add aws-lambda @types/aws-lambda\n```\n\nInstalls the code you need to interact with the AWS Lambda environment.\n\n### 2. add a scraper function\n\nDefine a new scraper function in `serverless.yml`\n\n```yaml\n# serverless.yml\n\nfunctions:\n  scraper:\n    handler: dist/scraper.handler\n    memorysize: 2536\n    timeout: 30\n    events:\n      - http:\n          path: scraper\n          method: GET\n          cors: true\n```\n\nWe're saying code lives in the `handler` method exported from `scraper`. We ask for lots of memory and a long timeout. Chrome is resource intensive and our code makes web requests, which might take a while.\n\nAll this fires from a GET request on `/scraper`.\n\n### 3. getChrome()\n\nThe `getChrome` method instantiates a new browser context. I like to put this in a `util` file.\n\n```typescript\n// src/util.ts\n\nimport chrome from \"chrome-aws-lambda\"\n\nexport async function getChrome() {\n  let browser = null\n\n  try {\n    browser = await chrome.puppeteer.launch({\n      args: chrome.args,\n      defaultViewport: {\n        width: 1920,\n        height: 1080,\n        isMobile: true,\n        deviceScaleFactor: 2,\n      },\n      executablePath: await chrome.executablePath,\n      headless: chrome.headless,\n      ignoreHTTPSErrors: true,\n    })\n  } catch (err) {\n    console.error(\"Error launching chrome\")\n    console.error(err)\n  }\n\n  return browser\n}\n```\n\nWe launch a Chrome Puppeteer instance with default config and specify our own screen size.\n\nThe `isMobile` setting tricks many websites into loading faster. The `deviceScaleFactor: 2` helps create better screenshots. It's like using a retina screen.\n\nAdding `ignoreHTTPSErrors` makes the process more robust.\n\nIf the browser fails to launch, we log debugging info.\n\n### 4. a shared createHandler()\n\nWe're building 2 pieces of code that share a lot of logic – scraping and screenshots. Both need a browser, deal with errors, and parse URL queries.\n\nWe build a common `createHandler()` method that deals with boilerplate and calls the important function when ready.\n\n```typescript\n// src/util.ts\n\nimport { APIGatewayEvent } from \"aws-lambda\"\nimport { Browser } from \"puppeteer\"\n\n// both scraper and screenshot have the same basic handler\n// they just call a different method to do things\nexport const createHandler = (\n  workFunction: (browser: Browser, search: string) => Promise<APIResponse>\n) => async (event: APIGatewayEvent): Promise<APIResponse> => {\n  const search =\n    event.queryStringParameters && event.queryStringParameters.search\n\n  if (!search) {\n    return {\n      statusCode: 400,\n      body: \"Please provide a ?search= parameter\",\n    }\n  }\n\n  const browser = await getChrome()\n\n  if (!browser) {\n    return {\n      statusCode: 500,\n      body: \"Error launching Chrome\",\n    }\n  }\n\n  try {\n    // call the function that does the real work\n    const response = await workFunction(browser, search)\n\n    return response\n  } catch (err) {\n    console.log(err)\n    return {\n      statusCode: 500,\n      body: \"Error scraping Google\",\n    }\n  }\n}\n```\n\nWe read the `?search=` param, open a browser, and verify everything's set up.\n\nThen we call the passed-in `workFunction`, which returns a response. If that fails, we throw a 500 error.\n\n### 5. scrapeGoogle()\n\nWe're ready to scrape Google search results.\n\n```typescript\nasync function scrapeGoogle(browser: Browser, search: string) {\n  const page = await browser.newPage()\n  await page.goto(\"https://google.com\", {\n    waitUntil: [\"domcontentloaded\", \"networkidle2\"],\n  })\n\n  // this part is specific to the page you're scraping\n  await page.type(\"input[type=text]\", search)\n\n  const [response] = await Promise.all([\n    page.waitForNavigation(),\n    page.click(\"input[type=submit]\"),\n  ])\n\n  if (!response.ok()) {\n    throw \"Couldn't get response\"\n  }\n\n  await page.goto(response.url())\n\n  // this part is very specific to the page you're scraping\n  const searchResults = await page.$$(\".rc\")\n\n  let links = await Promise.all(\n    searchResults.map(async (result) => {\n      return {\n        url: await result.$eval(\"a\", (node) => node.getAttribute(\"href\")),\n        title: await result.$eval(\"h3\", (node) => node.innerHTML),\n        description: await result.$eval(\"span.st\", (node) => node.innerHTML),\n      }\n    })\n  )\n\n  return {\n    statusCode: 200,\n    body: JSON.stringify(links),\n  }\n}\n\nexport const handler = createHandler(scrapeGoogle)\n```\n\nLots going on here. Let's go piece by piece.\n\n```typescript\nconst page = await browser.newPage()\nawait page.goto(\"https://google.com\", {\n  waitUntil: [\"domcontentloaded\", \"networkidle2\"],\n})\n```\n\nOpen a new page, navigate to google.com, wait for everything to load. I recommend waiting for `networkidle2`, which means all asynchronous requests have finished.\n\nUseful when dealing with complex webapps.\n\n```typescript\n// this part is specific to the page you're scraping\nawait page.type(\"input[type=text]\", search)\n\nconst [response] = await Promise.all([\n  page.waitForNavigation(),\n  page.click(\"input[type=submit]\"),\n])\n\nif (!response.ok()) {\n  throw \"Couldn't get response\"\n}\n\nawait page.goto(response.url())\n```\n\nTo scrape google, we type a search into the input field, then hit submit and wait for the page to load.\n\nThis part is different for every website.\n\n```typescript\n// this part is very specific to the page you're scraping\nconst searchResults = await page.$$(\".rc\")\n\nlet links = await Promise.all(\n  searchResults.map(async (result) => {\n    return {\n      url: await result.$eval(\"a\", (node) => node.getAttribute(\"href\")),\n      title: await result.$eval(\"h3\", (node) => node.innerHTML),\n      description: await result.$eval(\"span.st\", (node) => node.innerHTML),\n    }\n  })\n)\n\nreturn {\n  statusCode: 200,\n  body: JSON.stringify(links),\n}\n```\n\nWhen the results page loads, we:\n\n- look for every `.rc` DOM element – best identifier of search results I could find\n- iterate through results\n- get the info we want from each\n\nYou can use the `page.$eval` trick to parse DOM nodes with the same API you'd use in a browser. Executes your method on the nodes it finds and returns the result.\n\n### 6. hit deploy and try it out\n\nYou now have a bonafide web scraper. Wakes up on demand, runs chrome, turns Google search results into easy-to-use JSON.\n\nhttps://twitter.com/Swizec/status/1282446868950085632\n\nWe left out project configuration boilerplate. You can find those details in other chapters or [see example code on GitHub](https://github.com/Swizec/serverlesshandbook.dev/tree/master/examples/serverless-chrome-example).\n\n## Take screenshots\n\nTaking screenshots is similar to scraping. Instead of parsing the page, you call `.screenshot()` and get an image.\n\nOur example returns that image directly. You'll want to store on S3 and return a URL in a real project. Lambda isn't a great fit for large files.\n\n### 1. tell API Gateway to serve binary\n\nFirst, we tell API Gateway that it's okay to serve binary data.\n\nI don't recommend this in production unless you have a great reason. Like a dynamic image that changes every time.\n\n```yaml\n# serverless.yml\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: dev\n  apiGateway:\n    binaryMediaTypes:\n      - \"*/*\"\n```\n\nYou can limit `binaryMediaTypes` to specific types you intend to use. `*/*` is easier.\n\n### 2. add a new function\n\nNext we define a new Lambda function\n\n```yaml\n# serverless.yml\n\nfunctions:\n  screenshot:\n    handler: dist/screenshot.handler\n    memorysize: 2536\n    timeout: 30\n    events:\n      - http:\n          path: screenshot\n          method: GET\n          cors: true\n```\n\nSame as before, different name. Needs lots of memory and a long timeout.\n\n### 3. screenshotGoogle()\n\nWe're using similar machinery as before.\n\n```typescript\n// src/screenshot.ts\n\nasync function screenshotGoogle(browser: Browser, search: string) {\n  const page = await browser.newPage()\n  await page.goto(\"https://google.com\", {\n    waitUntil: [\"domcontentloaded\", \"networkidle2\"],\n  })\n\n  // this part is specific to the page you're screenshotting\n  await page.type(\"input[type=text]\", search)\n\n  const [response] = await Promise.all([\n    page.waitForNavigation(),\n    page.click(\"input[type=submit]\"),\n  ])\n\n  if (!response.ok()) {\n    throw \"Couldn't get response\"\n  }\n\n  await page.goto(response.url())\n\n  // this part is specific to the page you're screenshotting\n  const element = await page.$(\"#main\")\n\n  if (!element) {\n    throw \"Couldn't find results div\"\n  }\n\n  const boundingBox = await element.boundingBox()\n  const imagePath = `/tmp/screenshot-${new Date().getTime()}.png`\n\n  if (!boundingBox) {\n    throw \"Couldn't measure size of results div\"\n  }\n\n  await page.screenshot({\n    path: imagePath,\n    clip: boundingBox,\n  })\n\n  const data = fs.readFileSync(imagePath).toString(\"base64\")\n\n  return {\n    statusCode: 200,\n    headers: {\n      \"Content-Type\": \"image/png\",\n    },\n    body: data,\n    isBase64Encoded: true,\n  }\n}\n\nexport const handler = createHandler(screenshotGoogle)\n```\n\nSame code up to when we load the results page. Type a query, hit submit, wait for reload.\n\nThen we do something different – measure the size of our results div.\n\n```typescript\n// this part is specific to the page you're screenshotting\nconst element = await page.$(\"#main\")\n\nif (!element) {\n  throw \"Couldn't find results div\"\n}\n\nconst boundingBox = await element.boundingBox()\nconst imagePath = `/tmp/screenshot-${new Date().getTime()}.png`\n\nif (!boundingBox) {\n  throw \"Couldn't measure size of results div\"\n}\n```\n\nWe look for results and grab their `boundingBox()`. That tells us the `x, y` coordinates and the `width, height` size for a more focused screenshot.\n\nWe set up an `imagePath` in `/tmp`. We can write to a file on Lambda's hard drive, _but it will not stay there._ When our lambda turns off, the file is gone.\n\n```typescript\nawait page.screenshot({\n  path: imagePath,\n  clip: boundingBox,\n})\n```\n\nTake a screenshot with `page.screenshot()`. Saves to a file.\n\n```typescript\nconst data = fs.readFileSync(imagePath).toString(\"base64\")\n\nreturn {\n  statusCode: 200,\n  headers: {\n    \"Content-Type\": \"image/png\",\n  },\n  body: data,\n  isBase64Encoded: true,\n}\n```\n\nRead the file into a Base64-encoded string and return a response. It must contain a content type – `image/png` in our case – and tell API Gateway that it's Base64-encoded.\n\nThis is where you'd upload to S3 in production.\n\nYou can try mine here: [https://4tydwq78d9.execute-api.us-east-1.amazonaws.com/dev/screenshot](https://4tydwq78d9.execute-api.us-east-1.amazonaws.com/dev/screenshot)\n\n## How to use this\n\nThe most common use cases for Chrome Puppeteer are:\n\n1. Running automated tests\n2. Scraping websites cheaply\n3. Generating dynamic HTML-to-PNG images\n4. Generating PDFs\n\n3 and 4 are great because you can build a small website that renders a social card for your content and use this machinery to turn it into an image.\n\nSame for PDFs – build dynamic website, print-to-PDF with Chrome. Easier than generating PDFs by hand.\n\nHave fun 😊\n\nNext chapter we look at handling secrets."
  },
  {
    "path": "src/pages/serverless-dx/index.mdx",
    "content": "---\ntitle: \"Create a good serverless developer experience\"\ndescription: \"How to setup your project for a pleasant developer experience without servers\"\nimage: \"./img/good-dx.png\"\n---\n\nimport { TestCloudFunction } from \"../../components/TestCloudFunctions\"\n\n# Create a good serverless developer experience\n\n![](../../images/chapter_headers/good-dx.svg)\n\nWhat makes a good developer experience?\n\nI asked twitter and it was all over the place. A theme emerged:\n\n> Good developer experience is when tools make your job easier, get out of the way, and let you focus on _your_ code\n\nhttps://twitter.com/Swizec/status/1195742841877610496\n\nHow do you setup a serverless project for good DX?\n\nIt comes down to 3 features:\n\n1. Infrastructure-as-code\n2. Fast deploys\n3. Tooling for common tasks\n\n## Infrastructure-as-code\n\nhttps://twitter.com/muditameta/status/1195783892512444417\n\nAs mentioned in the [Getting Started](https://serverlesshandbook.dev/getting-started#setup-for-serverless-work) chapter, I like to use the open source [Serverless Framework](https://en.wikipedia.org/wiki/Serverless_Framework) with AWS. When using Netlify and Vercel you don't need Serverless because config-as-code is baked into their philosophies.\n\nYou write a configuration file, add it to version control, and that's your infrastructure. _Nothing_ happens outside that configuration file.\n\nThis means that:\n\n1. **Your deploys are repeatable**. Run deploy, get the same result every time. The same functions, the same queues, the same caching servers, everything.\n2. **Same infrastructure in test as in prod** Subtle differences between test environments and production are a waste of time. Big part of why Docker got popular.\n3. **Share infrastructure between the team**. Ever had to ask a team member what environment variable they used for a thing? I have. After 2 hours of digging into the problem and realizing it's a configuration issue. 🤦‍♂️\n4. **Infrastructure that always fits your feature branch** A common problem are new features with different infrastructure. Like adding a new queue or cloud function. Instead of setting it up every time you test, infra-as-code can do it for you.\n5. **Spend time in the tools you like, not confusing web UI** We're engineers and we like building things. Not clicking around a web UI doing repetitive tasks that take 20 minutes.\n\n## Fast deploys\n\nhttps://twitter.com/CodingDive/status/1195776781921464321\n\nThe shorter your feedback cycle, the faster you can work.\n\nOn the frontend we have local dev servers and hot reloading. You see the result almost as fast as you write the code.\n\nOn the backend things are trickier.\n\nYou make a change ... now what? If you have unit tests, they show you part of the picture. The specific scenarios you thought to test, the methods you're exercising, the particular inputs.\n\nAll great.\n\n![](https://media.giphy.com/media/WO74HAtUC9I40/giphy.gif)\n\nBut unit tests can't tell you your _system_ works. That's where bugs come from – systems complexity.\n\nYou can simulate the environment and run your tests. That works to an extent, but it's never perfect.\n\nYour best bet is to make deploying to a staging, QA, or production environment fast enough to use for development. With serverless, that becomes possible.\n\nYou could even set it up so that pushing to GitHub deploys every branch. Netlify and Vercel call it pull request previews.\n\n### How fast deploys work\n\nHere's how the flow works:\n\n0. Hit deploy\n1. **Compile your code locally** on your fast developer machine. Since your code is small, it compiles in seconds.\n2. **Compile your infrastructure** the serverless framework compiles your infrastructure into a config file for the target platform. With AWS that's [SAM](https://aws.amazon.com/serverless/sam/).\n3. **Upload your bundle** this is the slowest part.\n4. **Infrastructure sets itself up** using your config the platform sets itself up. Servers appear, queues go up, etc. Takes a few seconds\n5. **You're ready to go**\n\n<div id=\"lock\" />\n\nSlowest deploys I've seen on production-sized backends are in the 2 minute range. That's for a system with hundreds of lines of configuration.\n\nOn my side projects it's 30 seconds.\n\nThat's fantastic compared to a Heroku or Docker deploy that takes 20 minutes.\n\n## Tooling for common tasks\n\nI like to use my project's `package.json` as a collection of scripts for common tasks. `yarn` or `npm run` make them easy to run.\n\nThe most common is `yarn deploy`\n\n```\n# package.json\n\n\"scripts\": {\n\t\"build\": \"tsc build\",\n\t\"deploy\": \"npm run build && sls deploy\"\n}\n```\n\nWith those 2 lines you can deploy from any branch without worry that you'll forget to build your project first. The `build` script runs a typescript build and `sls deploy` runs a serverless deploy.\n\nThis part gets trickier when you use multiple environments. We'll talk about that in [the chapter on prod, QA, and staging environments](/dev-qa-prod).\n\nOther helpful tools I've set up for bigger projects include:\n\n- `yarn psql` to connect to my remote database\n- `reset-env` to reset a remote database for testing\n- `test-X` to run different tests against the server environment\n- `add-engineer` to add a new engineer-specific environment so everyone can test on their own\n\nAny time you find yourself running the same sequence of commands, you should consider adding them as a script to `package.json`. Give others that same super power :)\n\n# How this works in practice\n\n![](giphy:hello_world)\n\nIn the next few minutes you're going to build your first serverless backend. A service that says Hello 👋\n\nWe're using open source technologies and deploying on AWS Lambda. You can learn about other providers in the [Serverless Flavors](/serverless-flavors) chapter.\n\nYou'll need a computer configured for JavaScript development: Have nodejs installed, a code editor, and a terminal.\n\n### Setup for serverless work\n\nWhen working with serverless I like to use the open source [Serverless](https://github.com/serverless/serverless) framework. We'll talk more about why in the [Good serverless dev experience](/serverless-dx) chapter.\n\nWith the serverless framework we're going to configure servers using YAML files. You write config, framework figures out the rest.\n\nInstall it globally:\n\n```sh\nnpm install -g serverless\n```\n\nYou'll need AWS credentials too.\n\nI recommend following [Serverless's guide on AWS setup](https://serverless.com/framework/docs/providers/aws/guide/credentials/). It walks you through the necessary steps on your Amazon account and a couple terminal commands to run.\n\n### Create a tiny project\n\nThere are no special initializers for serverless projects. You start with a directory and add a configuration file.\n\n![](../../images/start-serverless.gif)\n\n```sh\nmkdir hello-world\ncd hello-world\ntouch serverless.yml\ntouch handler.js\n```\n\nYou now have a project with 2 files:\n\n- `serverless.yml` for configuration\n- `handler.js` for server code\n\nIn future chapters you'll write backends using TypeScript. But one thing at a time :)\n\n### Configure your first server\n\nConfiguration for your server goes in `serverless.yml`. We're telling the Serverless framework that we want to use AWS, run nodejs, and that this is a dev project.\n\nThen we'll tell it where to find the code.\n\n```yaml\n# serverless.yml\n\nservice: hello-world\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: dev\n```\n\nOur service is called `hello-world` and there's a couple details about our provider. The `stage` tells the difference between development, QA, and production deployments. More on that in the [Dev, QA, and prod](/dev-qa-prod) chapter.\n\n#### Let's tell our server how to run code.\n\n```yaml\n# serverless.yml\n\nservice: hello-world\n\nprovider:\n    name: aws\n    runtime: nodejs12.x\n    stage: dev\n\nfunctions:\n    hello:\n        handler: ./handler.hello\n        events:\n            - http:\n\t              path: hello\n\t              method: GET\n\t              cors: true\n```\n\nWe started a `functions` section.\n\nEach entry becomes its own tiny server – a serverless lambda. Together, they're the `hello-world` service.\n\nThe `hello` lambda calls an exported `hello` function inside our `handler.js` file when a GET request hits `/hello`.\n\nAll that from these few lines of code 👌\n\n_PS: enabling [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) lets you call this function from other websites. Like your frontend app._\n\n### Write your first backend function\n\nBackend functions in a serverless environment look like the JavaScript functions you're used to. Grab arguments, return a response.\n\nAdd a hello function to `handler.js`\n\n```javascript\n// handler.js\n\nexports.hello = async (event) => {\n  return {\n    statusCode: 200,\n    body: \"Hello 👋\",\n  }\n}\n```\n\nIt's an async function that accepts a trigger event and returns a response. A success status with a `Hello 👋` body.\n\nThat's it. You wrote backend code. 🤘\n\n### Deploy your first serverless backend\n\nTo deploy, we run `serverless deploy`.\n\n![](../../images/deploy-serverless.gif)\n\nAnd your server is up.\n\nYou get a URL for your lambda and some debugging output. My URL is `https://z7pc0lqnw9.execute-api.us-east-1.amazonaws.com/dev/hello`, if you open it in your browser, it says `Hello 👋`\n\n<TestCloudFunction\n  serviceName=\"serverless-hello-world\"\n  urlPlaceholder=\"https://z7pc0lqnw9.execute-api.us-east-1.amazonaws.com/dev/hello\"\n  defaults\n/>\n\nI'll keep it up because it's free unless somebody clicks. And when they do, current AWS pricing gives me 1,000,000 clicks per month for free 😛\n\n### What you got\n\nThe Serverless framework talked to AWS and configured many things.\n\n![](../../images/hello-world-lambda.png)\n\n- **API Gateway** to proxy requests from the internet to your function\n- **Lambda** to run your code. This is a tiny container that wakes up when called.\n- **CloudWatch logs** to collect logs from your code. Helps with debugging.\n\nAll those are configured for you. No UI to click through, no config to forget about next time, nothing your friends have to set up to deploy the same code.\n\n![](../../images/hello-world.png)\n\nExciting!\n\nNext chapter, we talk about designing your serverless architecture.\n"
  },
  {
    "path": "src/pages/serverless-elements/index.mdx",
    "content": "---\ntitle: \"Elements of serverless – lambdas, queues, gateways, and more\"\ndescription: \"Learn about the elements of your serverless ecosystem and how they fit together\"\nimage: \"./img/serverless-elements.png\"\n---\n\n# Elements of serverless – lambdas, queues, gateways, and more\n\n![](../../images/chapter_headers/serverless-elements.svg)\n\nServerless is about combining small elements into a whole. But what are the elements and how do they fit together?\n\nWe mentioned lambdas, queues, and a few other in previous chapters – [Architecture Principles](/serverless-architecture-principles) and [Serverless Flavors](/serverless-flavors). Let's see how they work.\n\n## Lambda – a cloud function\n\n\"Lambda\" comes from [lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus) – a mathematical definition of functional programming that Alonzo Church introduced in the 1930s. It's an alternative to Turing's [turing machines](https://en.wikipedia.org/wiki/Turing_machine). Both describe a system that can [solve any solvable problem](https://en.wikipedia.org/wiki/Church%E2%80%93Turing_thesis). Turing machines use iterative step-by-step programming, lambda calculus uses functions-calling-functions programming.\n\nBoth are equal in power.\n\nAWS named their cloud functions AWS Lambda. As the platform grew in popularity, the word \"lambda\" morphed into a generic term for cloud functions. The core building block of serverless computing.\n\n**A lambda is a function.** In this context, a function running as its own tiny server triggered by an event.\n\nHere's a lambda function that returns `\"Hello world\"` in response to an HTTP request.\n\n```typescript\n// src/handler.ts\n\nimport { APIGatewayEvent } from \"aws-lambda\";\n\nexport const handler = async (event: APIGatewayEvent) => {\n\treturn {\n\t\tstatusCode: 200\n\t\tbody: \"Hello world\"\n\t}\n}\n```\n\nThe TypeScript file exports a function called `handler`. The function accepts an event and returns a response. The AWS Lambda platform handles the rest.\n\nBecause this is a user-facing API method, it accepts an AWS API Gateway event and returns an HTTP style response. Status code and body.\n\nOther providers and services have different events and expect different responses. A lambda always follows this pattern 👉 **function with an event and a return value**.\n\n### Considerations with lambda functions\n\nYour functions should follow functional programming principles:\n\n- **idempotent** – multiple calls with the same inputs produce the same result\n- **pure** – rely on the arguments you're given and nothing else. Your environment does not persist, data in local memory might vanish.\n- **light on side-effects** – you need side-effects to make changes like writing to a database. Make sure those come in the form of calling other functions and services. _State inside your lambda does not persist_\n- **do one thing and one thing only** – small functions focused on one task are easiest to understand and combine\n\nSmall functions work together to produce extraordinary results. Like this example of [combining Twilio and AWS Lambda to answer the door](https://swizec.com/blog/how-i-answer-the-door-with-aws-lambda-and-twilio/swizec/9255).\n\n### Creating lambdas\n\nIn the open source Serverless Framework, you define lambda functions with `serverless.yml` like this:\n\n```yaml\nfunctions:\n  helloworld:\n    handler: dist/helloworld.handler\n    events:\n      - http:\n          path: helloworld\n          method: GET\n          cors: true\n```\n\nDefine a `helloworld` function and say it maps to the `handler` method exported from `dist/helloworld`. We're using a build step for TypeScript – the code is in `src/`, we run it from `dist/`.\n\n`events` lists the triggers that run this function. An HTTP GET request on the path `/helloworld` in our case.\n\nOther typical triggers include Queues, S3 changes, CloudWatch events, and DynamoDB listeners. At least on AWS.\n\n## Queue\n\nQueue is short for [message queue](https://en.wikipedia.org/wiki/Message_queue) – a service built on top of [queue, the data structure](<https://en.wikipedia.org/wiki/Queue_(abstract_data_type)>). Software engineers aren't that inventive with names 🤷‍♂️\n\nYou can think of the queue data structure as a list of items.\n\n![](../../images/queue.png)\n\n`enqueing` adds items to the back of a queue, `dequeing` takes them out the front. Items in the middle wait their turn. Like a lunch-time burrito queue. First come first serve, FIFO for short (first in first out).\n\n<div id=\"lock\" />\n\n**A messaging queue takes this data structure and scales it into a service.**\n\nDifferent implementations exist and they all share these core properties:\n\n- **persistent storage** – queues have to be reliable and store messages in a database. Most queues prioritize speed and use in-memory storage like Redis.\n- **a worker process** – once you have messages, you need to process them. A process periodically checks, if there's something new [(polling)](<https://en.wikipedia.org/wiki/Polling_(computer_science)>). Another approach is to trigger this check every time a message arrives.\n- **a trigger API** – when the queue sees a new message, it runs your code. In serverless that means running your lambda, in traditional environments it's another worker process.\n- **a retry/error policy** – queues help you deal with errors. When you fail to process a message, it goes back on the queue and gets retried later.\n\nMany modern queues add time to the mix. You can _schedule_ messages for later. 2 seconds, 2 minutes, 2 days, ... Some queues limit how long messages can stick around.\n\n### Time helps with errors\n\nServer processes can [fail for any reason at any time](/architecture-principles). For temporary errors a queue can use [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) when retrying. Giving your system more and more time to recover from issues.\n\nTry to process a message. It fails. Try after 1 second. Fails. Retry in 2 seconds. Fail. 4 seconds ...\n\nI've seen corrupt messages, known as poison pills, backing off days into the future. We talk about handling unprocessable messages in the [Robust Backend Design](/robust-backend-design) chapter.\n\n### Defining a queue\n\nAWS SimpleQueueService is a great queue service for the AWS serverless ecosystem. More powerful alternatives exist, but require more setup, more upkeep, and have features you might not need.\n\n> Always use the simplest and smallest service that solves your problem until you know _why_ you need more ✌️\n\nUsing the serverless framework, you define an SQS queue in the `resources` section like this:\n\n```yaml\nresources:\n  Resources:\n    MyQueue:\n      Type: \"AWS::SQS::Queue\"\n      Properties:\n        QueueName: \"MyQueue-${self:provider.stage}\"\n```\n\nA resource named `MyQueue` of the SQS Queue type with a queue name of `MyQueue-{stage}`. Adding the stage to your queue name allows different logical environments without collisions. More on that in [the Dev, QA, and Prod chapter](/dev-qa-prod)\n\nMake sure you use correct capitalization. The second `Resources` must be capitalized, so do `Type`, `Properties`, and `QueueName`.\n\nCapitalization matters because you're dropping a layer below Serverless and writing AWS's native Serverless Application Model (SAM) configuration. It's what your config compiles to before deploying.\n\n### Processing a queue\n\nYou need a lambda to process messages on `MyQueue`.\n\n```yaml\nfunctions:\n  myQueueProcess:\n    handler: dist/lambdas/myQueue.handler\n    events:\n      - sqs:\n          arn:\n            Fn::GetAtt:\n              - MyQueue\n              - Arn\n          batchSize: 1\n```\n\nA lambda that runs each time `MyQueue` has a new message to process. With a `batchSize` of 1, each message runs its own lambda – a good practice for initial implementations. More about batch sizes in the [Lambda Workflows](/lambda-workflows) and [Robust Backend Design](/robust-backend-design) chapters.\n\nThe strange yaml syntax reads as: _an SQS event fired by a queue with the ARN identifier of getAttribute(Arn) from MyQueue_. Amazon Resource Names, ARN, are unique identifiers for each resource in your AWS account.\n\nThe `myQueue.handler` lambda would look like this:\n\n```typescript\nimport { SQSEvent, SQSRecord } from \"aws-lambda\"\n\nexport const handler = async (event: SQSEvent) => {\n  // N depends on batchSize setting\n  const messages: string[] = event.Records.map(\n    (record: SQSRecord) => record.body\n  )\n\n  // do something with each message\n  // throw Error() on fail\n\n  return true\n}\n```\n\nAn async function that accepts an `SQSEvent`, which contains multiple messages depending on `batchSize`. Never assume the total number because that setting might change.\n\nExtract messages into an array of strings and process with a loop. Throw an error when something goes wrong so SQS can retry. Return true on success.\n\n### SNS\n\nA useful alternative to SQS is the Simple Notification Service – SNS. Similar behavior, except you can't store messages on the queue.\n\nWith SNS each message sends _once_. If you don't catch it, it's gone.\n\nBut unlike SQS, each message can trigger multiple services.\n\n## API Gateway\n\nYou might not realize this, but servers don't talk directly to the internet.\n\n![](giphy:whaaat)\n\nApplication servers are protected by a [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy) – a server that specializes in taking raw internet requests and routing them.\n\n![](../../images/api-gateway.png)\n\nRequests come from the wild internet into the proxy. The proxy then decides:\n\n- is this a valid request?\n- will this request break the system?\n- which type of server can handle this?\n- which instance of that server should do it?\n\nValidating requests can involve checking for denial of service attacks, verifying permissions, firewall protection, and even A/B testing.\n\nOnce verified the request goes to an instance of your application server. This is where [horizontal scaling](https://en.wikipedia.org/wiki/Scalability#HORIZONTAL-SCALING) comes into play.\n\nAmazon calls their reverse proxy service API Gateway.\n\nOther providers might have different names and they all perform the same function: Take request from the internet and pass it on to your lambda.\n\n## Static file storage – S3\n\nThe serverless world is ephemeral which means you can't save files just anywhere.\n\nAdding large files to your code makes starting new containers slow. You can't save locally because your server disappears after each request.\n\nStatic file storage services solve that problem.\n\nAWS S3 is the most common. Other hosting services often act as an S3 abstraction. Pundits have called S3 the 8th wonder of the world.\n\nhttps://twitter.com/QuinnyPig/status/1220805676168970246\n\nYou can think of S3 as a hard drive with an API. Read and write files, get their URL, change permissions, etc.\n\nEach file gets a URL that's backed by a server optimized for static files. No code, no dynamic changes. A raw file flying through HTTP.\n\n## Static file server – CDN\n\n![](../../images/cdn.png)\n\nCDNs – [content delivery networks](https://en.wikipedia.org/wiki/Content_delivery_network) – are the next step in serving static file. They're like a distributed caching system.\n\nWhere S3 serves files from a central location, a CDN serves those same files from as close to the end user as possible. That speeds up your website by reducing latency.\n\nConfiguration on your end goes like this:\n\n1. Point the CDN to your static file URL\n2. The CDN gives you a new URL\n3. Use _that_ URL in your client-side code\n\nYou can automate this part with build tools. Netlify and Vercel both handle it for you.\n\nNow when a browser requests a file, the URL resolves to the nearest server. Request goes to that server and if the file is there, it's served. If there's no file, the CDN goes to your original source, caches the file, and _then_ sends it back to the user.\n\nAnd now your JavaScript, HTML, images, fonts, and CSS are fast to load anywhere in the world. 👌\n\n## Logging\n\nLogging is one of the hardest problems in a distributed multi-service world. You can't print to the console or write to a local file because you can't see the console and files vanish after every request.\n\nWhat do you do?\n\nYou use a logging service to send logs to a central location.\n\nImplementing a logging service yourself is tricky (I've done it, do not recommend). The AWS ecosystem has you covered with CloudWatch.\n\nCloudWatch UI tools lack filtering and graphing features you'd want as a power user, but it's a great start.\n\nAnything you `console.log` is collected in CloudWatch alongside default logs.\n\n# Those are the important elements\n\nNow you know the most important elements of your serverless ecosystem:\n\n- lambdas for doing things\n- queues for communicating\n- gateways for handling requests\n- S3 for static files\n- CDN for serving static files\n- logging to keep track\n\nThere's a bunch more to discover, but that's the core. Next chapter we look at using these to build a robust system.\n"
  },
  {
    "path": "src/pages/serverless-flavors/index.mdx",
    "content": "---\ntitle: \"AWS, Azure, Vercel, Netlify, or Firebase?\"\ndescription: \"How do you choose between serverless providers?\"\nimage: \"./img/serverless-flavors.png\"\n---\n\n# AWS, Azure, Vercel, Netlify, or Firebase?\n\n![](../../images/chapter_headers/serverless-flavors.svg)\n\nYou've read the [getting started](/getting-started) of serverless, you know the [pros and cons](/serverless-pros-cons), and decided to use serverless in your next project.\n\nNow what?\n\nThere's a bunch of providers with different features, different pricing, different developer experience, different focus ... how do you choose?\n\nHaving tried many of those providers myself (AWS, Netlify, Vercel, Firebase), here's how I think about it 👇\n\n## AWS\n\n[![](../../images/aws.png)](https://aws.amazon.com)\n\nAWS is the serverless workhorse. They offer everything from function-as-a-service to hosted blockchain and machine learning products.\n\nMany other hosting providers use AWS. Heroku runs their dynos on EC2 instances, Netlify and Vercel use S3 for static files, Lambda for cloud functions, etc. The exact details are a secret, but we can guess.\n\nDid you know AWS was [more than half of Amazon's revenue](https://www.itproportal.com/news/aws-now-makes-up-over-half-of-all-amazon-revenue/) in 2019? It's a beast.\n\nWith over 165 services, it's impossible to try or even know all of AWS. A few that I've used are:\n\n- **EC2** – old school cloud. You get a virtual computer, set it up, and you're in control. Runs forever unless you make it stop.\n- **S3** – the standard solution for static files. Upload a file, get a URL, file stays there forever. Used for image and video assets, but can't run server code or host a website.\n- **CloudFront** – a [CDN](https://en.wikipedia.org/wiki/Content_delivery_network) that integrates with S3. Point to static files via CloudFront and they go to a server nearest to your users. Works like a read-through cache, makes your apps faster.\n- **IAM** – identity and account management. AWS forces you to use this to manage permissions. It's secure, tedious to set up, and a thorn in your butt. Until it saves your butt.\n- **AWS Secrets Manager** – a secure way to use secrets in your serverless apps. Not in code, not in environment variables, but a secure encrypted storage. \n- **Lambda** – the poster child for serverless. One of the first to popularize function-as-a-service architectures. Write your function, Lambda handles the rest.\n- **SQS** – simple queue service is a simple way to implement queues. It's best used for communication between services. Stores and retries messages if necessary.\n- **SNS** – simple notification service. Similar to SQS but designed for broadcasting. Many listeners can read each message and every message is delivered just once.\n- **DynamoDB** – a hosted JSON document storage. One of the quickest-to-setup ways to persist data in a serverless application. Save a JSON blob and read it later. Not recommended for relational data models.\n- **RDS** – relational database service. Set up a Postgres or MySQL database and AWS handles the rest. Better fit for traditional data with lots of relations. \n- **CloudWatch** – a logging system. Every service in AWS connects to CloudWatch to dump logs and other debug data. You can use it to trigger lambdas on a schedule.\n\nAWS services add up fast. Every tool does *one* job. No tool does *your* job.\n\n### When to choose AWS\n\nI use AWS when I want control over the developer experience and how the system fits together. \n\nFor example: I'd use AWS when my project involves data pipelines, coordinating between users, and complex backend logic. You know it's backend logic because it impacts multiple users on different devices.\n\n### When not to choose AWS\n\nWhere AWS becomes overkill are typical JAMstack apps. A static site with lots of frontend logic.\n\nHosting those on AWS is a pain whereas Netlify and Vercel make them a core feature.\n\n## Azure\n\n[![](../../images/azure.png)](https://azure.microsoft.com)\n\nAzure ... exists. Microsoft's answer to AWS and it's hard to find people in the wild who use it. \n\nPopular in the enterprise world with companies that can't or won't use Amazon services. I would stay away unless there's a good reason.\n\n## Firebase\n\n[![](../..images/firebase.png)](https://firebase.google.com/)\n\n<div id=\"lock\" />\n\nFirebase is Google's answer to AWS. Kind of.\n\nYou can think of Firebase as a done-for-you backend for web and mobile apps. Set up an account, pick your options, and it's ready to go.\n\nRight now there are 18 firebase services ranging from analytics to database and even machine learning. I've tried their database solution and it works great.\n\nThis is less extensive than the AWS section and that's the point. Firebase doesn't want you to think about any of this.\n\n### When to choose Firebase\n\nYou get the backend in a box with Firebase. There's little setup.\n\nYou'll have to change how you write your frontend code so it hooks up with Firebase and ... that's about it. \n\nGreat for small demos and when you don't want to think about the backend at all.\n\n### When not to choose Firebase\n\nLike with other Magic providers, you're in trouble as soon as you step off the beaten path. Or when you have a special requirement they didn't predict.\n\nWhen that happens your choice is to either change your app or rebuild from scratch.\n\n## Netlify\n\n[![](../../images/netlify.png)](https://www.netlify.com/)\n\nNetlify is wonderful. They invented everyone's favorite [JAMstack](https://en.wikipedia.org/wiki/Netlify#JAMstack) buzzword. \n\nStarting as a static site host for JavaScript-heavy applications, Netlify now offers cloud functions, authentication, and other backend functionality. Meant for simple backend tasks.\n\nAmong Netlify's biggest draws are:\n\n- a great web UI to manage your projects\n- git integration for automated deploys\n- custom URLs for every branch\n- automatic CDN and SSL setup for your site\n- app builds run on their servers\n- you can run their infrastructure locally\n- PR deploy previews\n- great support for teams\n\n### When to choose Netlify\n\nNetlify is one of the best funded startups in this arena. That can be an important factor.\n\nThey've been around long enough to rely on and are young enough that you'll get decent support, if you have issues. I find that to be a great balance :)\n\nIf you like a git- and UI- first mentality, Netlify is great.\n\n### When not to choose Netlify\n\nNetlify is almost always a great choice for your web app. Their cloud function support can be cumbersome and malnourished. It doesn't feel like a focus.\n\nIf you like interacting with your sites via the command line, I've found Netlify to be less great.\n\n## Vercel\n\n[![](../../images/zeit.png)](https://vercel.com/)\n\nVercel is wonderful. Netlify's direct competitor created by Silicon Valley veterans.\n\nVercel's focus, however, is different than Netlify's. \n\nWhere Netlify makes static deploys painless to configure and deploy via git and their UI, Vercel tries to make *any* app painless to deploy via their CLI. Command-line first, git and UI second.\n\nVercel's cloud function support is similar to Netlify's. Meant for small functionality and help with frontend tasks. \n\nAmong Vercel's biggest draws are:\n\n- superb command line interface\n- configure-less deploys\n- git integration for automated deploys\n- custom URLs for every deploy\n- automatic CDN and SSL setup for your site\n- PR deploy previews\n- app builds run on their servers\n- great support for teams\n\n### When to choose Vercel\n\nVercel is almost always a great choice for your web app. \n\nHonestly it's a toss up between Netlify and Vercel right now. They're both playing catch up. When one releases a new feature, the other gets it a few months later.\n\nI like Vercel's command line interface and the fact I can run `vercel` in any project and it shows up on the internet. No clicking or config needed.\n\n### When not to choose Vercel\n\nVercel is best for frontend-heavy apps and when you're using their NextJS framework. Like Netlify, it feels unlikely their backend support will reach the full power of AWS.\n\nAny project that fits on Netlify, fits on Vercel. Then it becomes a matter of preference and familiarity. Personally, I like Vercel's aesthetic and CLI focus. If you prefer clicking in a UI, go with Netlify.\n\n## So ... what to choose?\n\nMy preference is to put the frontend on Netlify or Vercel and the backend on AWS.\n\nThis gives me a balance between control, simplicity, and developer experience. We look at that in the next chapter.\n"
  },
  {
    "path": "src/pages/serverless-graphql/index.mdx",
    "content": "---\ntitle: \"Serverless GraphQL API\"\ndescription: \"Learn why GraphQL, how GraphQL, should it replace REST, how to choose, and implement it with Serverless\"\nimage: \"./img/serverless-graphql.png\"\n---\n\n# Serverless GraphQL API\n\n![](../../images/chapter_headers/serverless-graphql.svg)\n\nSay you're building an app. It needs data from a server. What do you do?\n\nYou make a `fetch()` request.\n\n```javascript\nfetch(\"https://swapi.dev/api/people/1/\")\n  .then((res) => res.json())\n  .then(console.log)\n```\n\nAnd you get eeeevery piece of info about Luke Skywalker.\n\n```json\n{\n  \"name\": \"Luke Skywalker\",\n  \"height\": \"172\",\n  \"mass\": \"77\",\n  \"hair_color\": \"blond\",\n  \"skin_color\": \"fair\",\n  \"eye_color\": \"blue\",\n  \"birth_year\": \"19BBY\",\n  \"gender\": \"male\",\n  \"homeworld\": \"https://swapi.dev/api/planets/1/\",\n  \"films\": [\n    \"https://swapi.dev/api/films/2/\",\n    \"https://swapi.dev/api/films/6/\",\n    \"https://swapi.dev/api/films/3/\",\n    \"https://swapi.dev/api/films/1/\",\n    \"https://swapi.dev/api/films/7/\"\n  ],\n  \"species\": [\"https://swapi.dev/api/species/1/\"],\n  \"vehicles\": [\n    \"https://swapi.dev/api/vehicles/14/\",\n    \"https://swapi.dev/api/vehicles/30/\"\n  ],\n  \"starships\": [\n    \"https://swapi.dev/api/starships/12/\",\n    \"https://swapi.dev/api/starships/22/\"\n  ],\n  \"created\": \"2014-12-09T13:50:51.644000Z\",\n  \"edited\": \"2014-12-20T21:17:56.891000Z\",\n  \"url\": \"https://swapi.dev/api/people/1/\"\n}\n```\n\nFrustrating ... all you wanted was his name and hair color. Why's the API sending you all this crap? 🤦‍♂️\n\nAnd what's this about Luke's species being `1`? What the heck is `1`?\n\nYou make another fetch request.\n\n```javascript\nfetch(\"https://swapi.dev/api/species/1/\")\n  .then((res) => res.json())\n  .then(console.log)\n```\n\nYou get data about humans. Great.\n\n```json\n{\n  \"name\": \"Human\",\n  \"classification\": \"mammal\",\n  \"designation\": \"sentient\",\n  \"average_height\": \"180\",\n  \"skin_colors\": \"caucasian, black, asian, hispanic\",\n  \"hair_colors\": \"blonde, brown, black, red\",\n  \"eye_colors\": \"brown, blue, green, hazel, grey, amber\",\n  \"average_lifespan\": \"120\",\n  \"homeworld\": \"https://swapi.dev/api/planets/9/\",\n  \"language\": \"Galactic Basic\",\n  \"people\": [\n    \"https://swapi.dev/api/people/1/\",\n    \"https://swapi.dev/api/people/4/\",\n    \"https://swapi.dev/api/people/5/\",\n    \"https://swapi.dev/api/people/6/\",\n    \"https://swapi.dev/api/people/7/\",\n    \"https://swapi.dev/api/people/9/\",\n    \"https://swapi.dev/api/people/10/\",\n    \"https://swapi.dev/api/people/11/\",\n    \"https://swapi.dev/api/people/12/\",\n    \"https://swapi.dev/api/people/14/\",\n    \"https://swapi.dev/api/people/18/\",\n    \"https://swapi.dev/api/people/19/\",\n    \"https://swapi.dev/api/people/21/\",\n    \"https://swapi.dev/api/people/22/\",\n    \"https://swapi.dev/api/people/25/\",\n    \"https://swapi.dev/api/people/26/\",\n    \"https://swapi.dev/api/people/28/\",\n    \"https://swapi.dev/api/people/29/\",\n    \"https://swapi.dev/api/people/32/\",\n    \"https://swapi.dev/api/people/34/\",\n    \"https://swapi.dev/api/people/43/\",\n    \"https://swapi.dev/api/people/51/\",\n    \"https://swapi.dev/api/people/60/\",\n    \"https://swapi.dev/api/people/61/\",\n    \"https://swapi.dev/api/people/62/\",\n    \"https://swapi.dev/api/people/66/\",\n    \"https://swapi.dev/api/people/67/\",\n    \"https://swapi.dev/api/people/68/\",\n    \"https://swapi.dev/api/people/69/\",\n    \"https://swapi.dev/api/people/74/\",\n    \"https://swapi.dev/api/people/81/\",\n    \"https://swapi.dev/api/people/84/\",\n    \"https://swapi.dev/api/people/85/\",\n    \"https://swapi.dev/api/people/86/\",\n    \"https://swapi.dev/api/people/35/\"\n  ],\n  \"films\": [\n    \"https://swapi.dev/api/films/2/\",\n    \"https://swapi.dev/api/films/7/\",\n    \"https://swapi.dev/api/films/5/\",\n    \"https://swapi.dev/api/films/4/\",\n    \"https://swapi.dev/api/films/6/\",\n    \"https://swapi.dev/api/films/3/\",\n    \"https://swapi.dev/api/films/1/\"\n  ],\n  \"created\": \"2014-12-10T13:52:11.567000Z\",\n  \"edited\": \"2015-04-17T06:59:55.850671Z\",\n  \"url\": \"https://swapi.dev/api/species/1/\"\n}\n```\n\nThat's a lot of JSON to get the word `\"Human\"` out of the [Star Wars API](https://swapi.dev/) ...\n\n![](giphy:eyeroll)\n\nWhat about Luke's starships? There's 2 and that means 2 more API requests ...\n\n```javascript\nfetch(\"https://swapi.dev/api/starships/12/\")\n  .then((res) => res.json())\n  .then(console.log)\n\nfetch(\"https://swapi.dev/api/starships/22/\")\n  .then((res) => res.json())\n  .then(console.log)\n```\n\nWanna know how much JSON that dumps? Try a guess. 🙄\n\nYou made **4 API requests** and transferred a bunch of data to find out that Luke Skywalker is human, has blond hair, and flies an X-Wing and an Imperial Shuttle.\n\nAnd guess what, you didn't cache anything. How often do you think this data changes? Once a year? Twice?\n\n🤦‍♂️\n\n## GraphQL to the rescue\n\nHere's what the same process looks like with GraphQL.\n\n```graphql\nquery luke {\n  // id found through allPeople query\n  person(id: \"cGVvcGxlOjE=\") {\n    name\n    hairColor\n    species {\n      name\n    }\n    starshipConnection {\n      starships {\n        name\n      }\n    }\n  }\n}\n```\n\nAnd the API returns what you wanted with 1 request.\n\n```json\n{\n  \"data\": {\n    \"person\": {\n      \"name\": \"Luke Skywalker\",\n      \"hairColor\": \"blond\",\n      \"species\": null,\n      \"starshipConnection\": {\n        \"starships\": [\n          {\n            \"name\": \"X-wing\"\n          },\n          {\n            \"name\": \"Imperial shuttle\"\n          }\n        ]\n      }\n    }\n  }\n}\n```\n\nAn API mechanism that gives you flexibility on the frontend, slashes API requests, _and doesn't transfer data you don't need?_\n\n😲\n\nWrite a query, say what you want, send to an endpoint, GraphQL figures out the rest. Want different params? Just say so. Want multiple models? Got it. Wanna go deep? You can.\n\nWithout changes on the server. Within reason.\n\nGraphQL client libraries can add caching to avoid duplicate requests. And because queries are structured, clients can merge queries into 1 API call.\n\nI fell in love the moment it clicked.\n\n![](https://media.giphy.com/media/7FyMQm2vBiTjG/giphy.gif)\n\nYou can [try it on graphql.org's public playground](http://graphql.org/swapi-graphql)\n\n<div id=\"lock\" />\n\n## What _is_ GraphQL\n\n[GraphQL](https://en.wikipedia.org/wiki/GraphQL) is an open-source data query and manipulation language for APIs and a runtime for fulfilling queries with existing data.\n\nTouted as a replacement for REST, GraphQL is actually a different approach to APIs. Better for common use-cases and with its own drawbacks.\n\nDon't let the propaganda fool you 👉 you can and *should* use both REST *and* GraphQL. Depends on what you're doing.\n\nGraphQL's benefit is its declarative nature.\n\nOn the client, you describe the shape of what you want and GraphQL figures it out. On the server, you write resolver functions for sub-queries and GraphQL combines them into the full result.\n\nREST queries can be declarative on the client, while GraphQL is declarative on both ends. For reads _and_ writes.\n\n### GraphQL queries\n\nGraphQL queries fetch data. Following this pattern:\n\n```graphql\nquery {\n  what_you_want {\n    its_property\n  }\n}\n```\n\nYou nest fields – representing your models – and properties into queries to describe what you're looking for. You can go as deep as you want.\n\nWrite fields side-by-side to execute multiple queries with a single API request.\n\n```graphql\nquery {\n  what_you_want {\n    its_property\n  }\n  other_thing_you_want {\n    its_property {\n      property_of_property\n    }\n  }\n}\n```\n\nThis is where the power and flexibility come from.\n\nVariables let you create dynamic queries and build complex filters to limit the scope of your result.\n\n```graphql\nquery queryName($filterProp: \"value\") {\n\tfield(filterProp: $filterProp) {\n\t\tproperty1\n\t\tproperty3\n\t}\n}\n```\n\n`$filterProp` defines a new variable that the `field()` lookup uses to filter results for those matching `\"value\"`.\n\nGraphQL comes with basic equality filters built-in and you're encouraged to add more in your resolvers. Typical projects choose to support sorting, greater-than, not-equals, etc.\n\n### GraphQL mutations\n\nGraphQL mutations write data. Following this pattern:\n\n```graphql\nmutation {\n  what_youre_updating(argument: \"value\", argument2: \"value 2\") {\n    prop_you_want_back\n  }\n}\n```\n\nLike a query with arguments, you specify what you're updating and pass your values. What's available depends on how mutation resolvers are implemented on the server.\n\nThe mutation body specifies what you'd like returned. Same as a query.\n\nLike queries, you can put mutations side-by-side and use variables.\n\n```\nmutation mutationName($argument: \"value\") {\n\twhat_youre_updating(argument: $argument) {\n\t\tprop_you_want_back\n\t}\n\n\tother_field(argument: $argument) {\n\t\treturn_prop\n\t}\n}\n```\n\n## GraphQL vs. REST, which to use and when\n\nThe GraphQL vs. REST debate is a false dichotomy.\n\nPeople like to say GraphQL is _replacing_ REST and that's not the case. GraphQL is _augmenting_ REST.\n\nWill GraphQL become the default API layer? It might.\n\nShould you rip out your REST API and rewrite for GraphQL? Please don't.\n\nGraphQL doesn't care where data comes from. Relational database, NoSQL database, files, a REST API, another GraphQL API ... All you need is a function that reads data in response to a query.\n\nYou can [start using GraphQL without changing your existing server](https://swizec.com/blog/how-you-can-start-using-graphql-today-without-changing-the-backend/) with a GraphQL middle layer. Create a GraphQL server on top of your REST API.\n\n![](giphy:wow)\n\n### How do you choose between REST and GraphQL?\n\nWhen building a typical CRUD – Create, Read, Update, Delete – API, I like to ask 6 questions:\n\n1. Is this a new project? _Default to GraphQL_\n2. Do I know in advance what clients are going to need? _REST is great_\n3. Am I fetching small subsets of large data? _GraphQL_\n4. Am I generating new values for each request? _REST_\n5. Updating a few properties on an object? _GraphQL_\n6. Submitting large payloads to be saved? _REST_\n\nUse upserts to deal with Create and Update. Avoid deletes.\n\n### Should you expose your entire data model verbatim?\n\nNo.\n\nExposing your full data model is a common mistake in API design. \n\nDefine a clean API using [domain driven design](https://en.wikipedia.org/wiki/Domain-driven_design). How your backend stores data for max performance and good database design differs from how clients think about that data.\n\nAn \"access to everything\" API style is hard to use, leads to security issues, is difficult to maintain, and tightly couples servers and clients. You'll have to update both server and client for any DB change. \n\nThen there's the question of derived and computed properties with complex business logic. You don't want to re-implement those on the client using raw data from your database.\n\n## How to create a serverless GraphQL server\n\n![Example create mutation from server below](../../images/graphql-playground.png)\n\nThere's many ways to build a GraphQL server, the nicest I've found is with [Apollo](https://www.apollographql.com/)'s [apollo-server-lambda](https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-lambda) package.\n\nYou follow a 4 step process:\n\n1. Add lambda to your `serverless.yml` file\n2. Initialize the Apollo server\n3. Specify your schema\n4. Add resolvers\n\nWe created a [small REST API](https://serverlesshandbook.dev/serverless-rest-api#build-a-simple-rest) in the previous chapter, let's recreate it with GraphQL. [Full code on GitHub](https://github.com/Swizec/serverlesshandbook.dev/tree/master/examples/serverless-graphql-example)\n\nApollo creates a GraphQL playground for us. You can try my implementation here:\n\n<iframe\n  src=\"https://yrqqg5l31m.execute-api.us-east-1.amazonaws.com/dev/graphql\"\n  style={{ width: \"120%\", border: 0, height: \"600px\" }}\n></iframe>\n\nChange the URL inside the playground to: `https://yrqqg5l31m.execute-api.us-east-1.amazonaws.com/dev/graphql`. Apollo drops the `/dev/` part.\n\n### serverless.yml\n\nWe define a new function in the `functions:` section.\n\n```yaml\n# serverless.yml\n\nfunctions:\n  graphql:\n    handler: dist/graphql.handler\n    events:\n      - http:\n          path: graphql\n          method: GET\n          cors: true\n      - http:\n          path: graphql\n          method: POST\n          cors: true\n```\n\nThe convention is to use the `/graphql` endpoint for everything. \n\nMake sure to define both `GET` and `POST` endpoints. `GET` serves the Apollo playground, `POST` handles the queries and mutations.\n\n### initialize Apollo server\n\nEvery Apollo server needs a schema, a resolvers object, and the initialized server. Like this:\n\n```typescript\n// src/graphql.ts\n\nimport { ApolloServer, gql } from \"apollo-server-lambda\"\n\n// this is where we define the shape of our API\nconst schema = gql``\n\n// this is where the shape maps to functions\nconst resolvers = {\n  Query: {},\n  Mutation: {},\n}\n\nconst server = new ApolloServer({ typeDefs: schema, resolvers })\n\nexport const handler = server.createHandler({\n  cors: {\n    origin: \"*\", // for security in production, lock this to your real URLs\n    credentials: true,\n  },\n})\n```\n\nThis server won't run because the `schema` and `resolvers` don't match. Resolvers have fields that the schema does not.\n\nWe'll add type definitions to the schema and a resolver for each `Query` and `Mutation`.\n\n### specify your schema\n\nYour schema defines the shape of your API. Every type of object your server returns and every query and mutation definition.\n\nYou get a type-safe API because GraphQL ensures objects follow this schema. Both when coming into the API and when flying out.\n\nTo mimic our [CRUD API from before](https://serverlesshandbook.dev/serverless-rest-api#build-a-simple-rest), we use a schema like this:\n\n```typescript\n// src/graphql.ts\n\nconst schema = gql`\n  type Item {\n    id: String\n    name: String\n    body: String\n    createdAt: String\n    updatedAt: String\n  }\n\n  type Query {\n    item(id: String!): Item\n  }\n\n  type Mutation {\n    updateItem(id: String, name: String, body: String): Item\n    deleteItem(id: String!): Item\n  }\n`\n```\n\nWith REST, users could store and retrieve arbitrary JSON blobs. GraphQL doesn't support that. Instead, we define an `Item` type with an arbitrary `body` string.\n\nYou can use that to store serialized JSON objects.\n\nEach item will have an `id`, a `name`, and a few timestamps managed by the server. Those help with debugging.\n\nOur `item()` query lets you retrieve items and requires an `id` to work. That's the exclamation point after `String`.\n\nThen we have a mutation for upserting items and a mutation for deleting.\n\n### create your resolvers\n\nWe have 1 query and 2 mutations. That means we'll need 3 resolver functions.\n\nWe can copy these from the [REST implementation](https://serverlesshandbook.dev/serverless-rest-api#build-a-simple-rest) and change how we get arguments.\n\nI like to put queries in a `src/queries.ts` file and mutations in a `src/mutations.ts`. You can organize this by model as your implementation grows.\n\n#### item()\n\nThe `item()` query is a basic fetch and looks like this:\n\n```typescript\n// src/queries.ts\n\nimport * as db from \"simple-dynamodb\"\n\nfunction remapProps(item: any) {\n  return {\n    ...item,\n    id: item.itemId,\n    name: item.itemName,\n  }\n}\n\n// fetch using item(id: String)\nexport const item = async (_: any, args: { id: string }) => {\n  const item = await db.getItem({\n    TableName: process.env.ITEM_TABLE!,\n    Key: {\n      itemId: args.id,\n    },\n  })\n\n  return remapProps(item.Item)\n}\n```\n\nWe can ignore Apollo's first resolver argument. The second argument is where query and mutation values come in.\n\nSince `id` is a required param, you don't have to check for nulls. GraphQL handles that before calling your resolver.\n\nYou can call `getItem` on DynamoDB, or run a SQL query for a relational database, and return the result. GraphQL handles the rest.\n\n**One niggle to note 👉** DynamoDB considers `name` and `id` reserved attributes. We remap them to `itemName` and `itemId` when saving and have to map them back when reading.\n\nThis is what I meant by _\"You don't want to expose raw database details to your API\"_ :)\n\n#### updateItem()\n\nThe `updateItem()` mutation is our upsert.\n\nIt creates a new item when you don't send an `id` and updates the existing item when you do. If no item is found, the mutation throws an error.\n\nAnd it handles `createdAt` and `updatedAt` timestamps.\n\n```\n// src/mutations.ts\n\ntype ItemArgs = {\n  id: string\n  name: string\n  body: string\n}\n\n// upsert an item\n// item(name, ...) or item(id, name, ...)\nexport const updateItem = async (_: any, args: ItemArgs) => {\n  let itemId = args.id ? args.id : uuidv4()\n\n  let createdAt = new Date().toISOString()\n\n  // find item if exists\n  if (args.id) {\n    const find = await db.getItem({\n      TableName: process.env.ITEM_TABLE!,\n      Key: { itemId },\n    })\n\n    if (find.Item) {\n      // save createdAt so we don't overwrite on update\n      createdAt = find.Item.createdAt\n    } else {\n      throw \"Item not found\"\n    }\n  }\n\n  const updateValues = {\n    itemName: args.name,\n    body: args.body,\n  }\n\n  const item = await db.updateItem({\n    TableName: process.env.ITEM_TABLE!,\n    Key: { itemId },\n    UpdateExpression: `SET ${db.buildExpression(\n      updateValues\n    )}, createdAt = :createdAt, updatedAt = :updatedAt`,\n    ExpressionAttributeValues: {\n      ...db.buildAttributes(updateValues),\n      \":createdAt\": createdAt,\n      \":updatedAt\": new Date().toISOString(),\n    },\n    ReturnValues: \"ALL_NEW\",\n  })\n\n  return remapProps(item.Attributes)\n}\n```\n\nWe take the id from arguments or create a new one with `uuidv4()`.\n\nThen we try to find the item. \n\nIf found, we change `createdAt` to the existing value. Otherwise we throw an error. That helps you avoid creating new items by accident because DynamoDB always upserts.\n\nWe define the `updateValues` and build a DynamoDB UPDATE query. This can look gnarly no matter what DB you use.\n\nIn the end, we return the object our database returned and let GraphQL handle the rest.\n\n#### deleteItem()\n\nThe `deleteItem()` mutation is simple. GraphQL and the database do the work for us.\n\n```typescript\n// src/mutations.ts\n\nexport const deleteItem = async (_: any, args: { id: string }) => {\n  // DynamoDB handles deleting already deleted objects, no error :)\n  const item = await db.deleteItem({\n    TableName: process.env.ITEM_TABLE!,\n    Key: {\n      itemId: args.id,\n    },\n    ReturnValues: \"ALL_OLD\",\n  })\n\n  return remapProps(item.Attributes)\n}\n```\n\nWe take the `id` from mutation arguments, ask our database to delete the row, and return the old attributes.\n\n### add resolvers to the GraphQL server\n\nWe built the resolvers, now we add them to `graphql.ts`:\n\n```typescript\n// src/graphql.ts\n\nimport { item } from \"./queries\"\nimport { updateItem, deleteItem } from \"./mutations\"\n\n// ...\n\nconst resolvers = {\n  Query: {\n    item,\n  },\n  Mutation: {\n    updateItem,\n    deleteItem,\n  },\n}\n```\n\nThis tells Apollo how to map queries and mutations to resolvers. We can use generic names because this is a small project.\n\nYou'll want to namespace in bigger apps.\n\n### Try it out\n\nRun `yarn deploy` and you get a GraphQL server. There's even an Apollo playground that helps you test.\n\n<iframe\n  src=\"https://yrqqg5l31m.execute-api.us-east-1.amazonaws.com/dev/graphql\"\n  style={{ width: \"120%\", border: 0, height: \"600px\" }}\n></iframe>\n\nMake sure you change the URL inside the playground to: `https://yrqqg5l31m.execute-api.us-east-1.amazonaws.com/dev/graphql`. Apollo drops the `/dev/` part.\n\n## GraphQL no-code services\n\nWiring up GraphQL can get tedious with lots of repetitive code.\n\nVarious no-code and low-code solutions exist that do the work for you. Look at your database and magic a GraphQL server into existence.\n\nThey tend to exhibit the *expose entire DB as API* problem. But they can be a good starting point.\n\nNext chapter we look at building lambda pipelines for distributed data processing.\n"
  },
  {
    "path": "src/pages/serverless-monitoring/index.mdx",
    "content": "---\ntitle: \"Monitoring serverless apps\"\ndescription: \"Server code is invisible, how do you know when it breaks?\"\nimage: \"./img/serverless-monitoring.png\"\n---\n\n# Monitoring serverless apps\n\n![](../../images/chapter_headers/serverless-monitoring.svg)\n\nWhen clients have a bug, you can tell. Go through the flow, click around, see what happens.\n\nBut code on the server is invisible. And [always broken](/robust-backend-design). A distributed system is never 100% error-free.\n\nA [good architecture](/serverless-architecture-principles) lets you ignore many errors. The system recovers on its own.\n\nWhat about the bad errors? And how do you debug code you can't see?\n\n## Observability\n\nObservability is the art of understanding the internal state of a system based on its outputs. It's a continuous process.\n\nA good system lets you:\n\n- understand what's going on\n- see trends\n- figure out what happened after an error\n- *predict* errors\n- know when there's an emergency\n- understand how to fix an emergency\n\nThose are design goals. There is no right answer. Observability is an art and getting it right takes practice.\n\nBut there are guidelines you can follow. You'll need:\n\n- **logs** are immutable events that happened in your system. They follow a structured format and offer information about what happened where and when.\n- **metrics** are aggregate events over time. They tell you how much of what is happening, how long it takes, and help you understand trends.\n- **traces** are journeys through the system. A sequence of events that contributed to a bigger result.\n\n<div id=\"lock\" />\n\n### Questions to ask yourself\n\nObservability is part of your development process. You can't tack it on later. \n\nI like to ask these questions when building:\n\n1. How will you know this works?\n2. How will you know this broke?\n3. How will you deduce where it broke?\n4. How will you figure out how it broke?\n5. How will you know which payload broke it?\n\nThere is no right answer. It takes a few emergencies to dial it in and save the info you need.\n\n### When do you need observability?\n\nAlways. 😛\n\nIt depends. How critical is your software?\n\nWhen you have 10 users, eh I'd focus on getting users. When you have 100 users, eh they'll tell you when there's a bug.\n\nYou'll see stranger and stranger bugs the more users you have. A 1-in-1000 bug happens every day when you have 1000 users. At Google scale, tiny impossible-to-reproduce bugs happen every minute.\n\nThat's when observability shines. Understanding bugs you can't reproduce.\n\n*PS: you don't need traditional \"monitoring\" in a serverless system. Your server is never down, your memory is never full, your storage never runs out, your CPU is never busy.*\n\n## What to measure\n\nDeciding what to measure is a art. You'll get it wrong.\n\nYou build a system asking the 5 questions we mentioned. Add a bunch of measurements and walk away.\n\nA few days pass and something goes wrong.\n\nYou go through the logs. There's too many. You ignore 80%. \n\nYou realize the 20% that are useful don't have enough info. Despite your best efforts, you can't be certain what happened.\n\nAdjust what you log, add the info you wish you had, remove the info you didn't need. Next time will be better.\n\nIt's an iterative process :)\n\n### Typical useful logs and events\n\nYou need two types of logs:\n\n1. The system is ticking along\n2. Errors\n\nHappy logs work like breadcrumbs. \n\nYou leave them behind so you can later trace a path through the system. How did this user get into that state? Are we seeing bottlenecks? Did event B that always comes after event A suddenly stop coming?\n\nYou want to know when a typical behavior changes.\n\nErrors – always log errors. Add as much debugging info as possible. Print the whole stack trace, the exact error, and any identifiers you'll need to reproduce the bug.\n\n### Metrics to track\n\nSpecific metrics depend on what you care about. \n\nGot a function that needs to be fast? Measure its speed. Got a suspected bottleneck? Measure requests waiting. Got a flaky process? Measure error rate.\n\nAt the least, you'll want to measure 3 metrics for each part of your system:\n\n1. **Throughput** – how many requests are you processing\n2. **Error rate** – how many errors happen\n3. **Failure rate** – how many requests never succeed\n\n*\"Part of the system\"* means an end-to-end process as seen by a user. Don't sweat individual pieces unless you identify a problem that needs a closer look.\n\n### When to alarm\n\nMetrics help when you look at them, logs help when you're solving a problem. Alarms come to you.\n\nAn escalating system works best:\n\n- email for small problems\n- slack when the fire grows\n- SMS for critical issues\n\nYou'll want to set alarms for high error and failure rates (depends what you consider high) and anomalies in throughput. When a 100/hour event drops to zero, something's wrong.\n\nHow to set alarms depends on your tool of choice. On AWS, CloudWatch offers basic support and I've loved DataDog in the past. Anomaly detection on DataDog is wonderful. 👌\n\n## Distributed logging\n\nLogging is the core of your observability toolbox. Metrics and traces build on top of logs.\n\nIn a serverless system, you can't sign into a server to see the logs. There's no server and your system is distributed across many services.\n\nYou'll need a distributed logging system.\n\nOn AWS, you can achieve this through CloudWatch. A service that collects output from your lambdas and offers a basic UI.\n\n### StatsD\n\nWhen you outgrow default CloudWatch metrics and need deeper insights, there's a rich ecosystem of tools and resources waiting for you. All built on top of a de facto standard: [StatsD](https://github.com/statsd/statsd).\n\nStatsD is an open source agent that listens for prints in a specific format to collect as metrics. It sends those to a central location without interfering with your code.\n\nYou can [use StatsD with CloudWatch](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-custom-metrics-statsd.html) to collect custom metrics. These show up in the CloudWatch UI.\n\nPrint logs like this:\n\n```\nconsole.log(\"MetricName:value|type|sample_rate|tag1,tag2\")\n```\n\nSample rate and tags are optional.\n\nWhen you print in that format, you can connect a number of 3rd party tools that give you power beyond the CloudWatch UI. [DataDog](https://www.datadoghq.com) has been a great choice for me.\n\n## Make a pulse dashboard\n\nThe final step in your observability journey is a nice dashboard. Something you can look at, see wiggly lines, and say *\"Yep, system's working\"*\n\nDashboard your critical metrics. What you're focusing on *right now*. 5 or 6 at most.\n\nYou can build detailed dashboards for specific parts of your system. That comes when your project grows.\n\nA typical core dashboard includes:\n\n- global request throughput\n- global error rate\n- global failure rate\n- response times\n\nAnd remember: A metric that isn't actionable is pure vanity and should be removed.\n\nNext chapter we look at the split between localhost and production."
  },
  {
    "path": "src/pages/serverless-performance/index.mdx",
    "content": "---\ntitle: \"Serverless performance\"\ndescription: \"How does performance impact your serverless setup? What should you think about?\"\nimage: \"./img/serverless-performance.png\"\n---\n\n# Serverless performance\n\n![](../../images/chapter_headers/serverless-performance.svg)\n\nPerformance talks about what you get per unit of a resource. Time, effort, space, energy, and money.\n\nHow much of X does it take to get N results?\n\nWe've touched on performance before. Mentioned scaling strategies in [Pros and Cons of Serverless](/serverless-pros-cons) and [Databases](/databases), talked about complexity in [Lambda Pipelines](/lambda-pipelines), and mentioned flow in [Robust Backend Design](/robust-backend-design).\n\nBut what do you measure? What's achievable? Where do you optimize? How does serverless stack up?\n\n## The performance trifecta\n\nSoftware systems care about time, money, and space. You have to balance all 3.\n\n![Fast, small, cheap](../../images/fast-small-cheap.png)\n\nWith metered pricing, serverless lets you pay directly for execution time, memory size, and storage space. No overhead. You don't use it, you don't pay it.\n\nAWS Lambda charges for execution time with [millisecond precision](https://aws.amazon.com/blogs/aws/new-for-aws-lambda-1ms-billing-granularity-adds-cost-savings/).\n\n**Faster code** uses more memory to gain speed. Or you can beef up CPU. Both make your code more expensive to run.\n\n**Saving storage space** costs CPU time to compress and clean data. Storage is cheap. Bandwidth to and from storage gets pricey.\n\nYou **optimize memory** with slower code. Memory is plentiful these days, but not in serverless. You want small functions.\n\nEvery optimization takes **human time and effort**. A metric that software _organizations_ care about. How hard is the code to develop and maintain?\n\nYou can think of performance as a 4-dimensional optimization function. **Speed vs. space vs. money vs. effort**. Great question for a calculus exam.\n\nAvoid doing the calculus with ye olde law of prioritization:\n\n1. Make it work\n2. Make it right\n3. Make it fast\n\nYou'll find that computers are fast & cheap and humans are slow & expensive. Stick to 1, add 2 for easier maintenance. 3 when necessary.\n\n### Where size matters\n\nStorage is cheap – [$0.023/GB/month on S3](https://aws.amazon.com/s3/pricing/) – and memory is _pretty_ cheap at [$0.000016/GB/second](https://aws.amazon.com/lambda/pricing/). Memory cost per unit _increases_ when you need extra. Storage cost per unit _decreases_ when you use more.\n\n![](giphy:shrug)\n\n_Bandwidth_ is where size gets you. AWS charges for taking data out of the system. Other providers are similar.\n\nYou pay to send data to users or between availability zones. Details depend on which services are involved. For example: S3 to CloudFront (the CDN) is free, then CloudFront charges $0.085/GB.\n\nMinimize what you send over the wire.\n\n## Thinking about speed\n\n2 metrics matter for \"speed\":\n\n1. Latency\n2. Throughput\n\nBest summarized with a metaphor from [Andrew S. Tanenbaum](https://en.wikipedia.org/wiki/Andrew_S._Tanenbaum):\n\n> Never underestimate the bandwidth of a station wagon full of tapes hurtling down the highway\n\n<div id=\"lock\" />\n\n### Latency\n\nLatency measures delay. How long does it take to start working?\n\nYou can write the fastest code in the world, but if it takes 2 seconds to get started you'll have unhappy users.\n\nThe biggest factors are:\n\n- network time\n- internal routing\n- lambda wake up time\n\n**Network time** measures how long it takes for a user's request to reach your server. Depends on distance and connection quality.\n\n**Routing** is internal to your serverless provider. How long does it take to accept a request and send it to your code? Security rules can make this slower or faster.\n\n**Lambda wake up time** measures how long it takes to spin up your tiny server. Depends on bundle size, runtime environment, and how your code warms up.\n\n### Throughput\n\nThroughput measures how fast you work. When you get a request, how long does it take?\n\nThe biggest factors are:\n\n- code performance\n- input/output\n\nServerless is for [small distributed operations](/lambda-pipelines). Code performance and algorithm complexity have little impact.\n\n> Fancy algorithms are slow when N is small, and N is usually small. Fancy algorithms have big constants. Until you know that N is frequently going to be big, don't get fancy.\n> ~ [Rob Pike](https://en.wikipedia.org/wiki/Rob_Pike)\n\nThat leaves **input/output**.\n\nWaiting for a database, talking to 3rd party APIs, loading a web page, those will _destroy_ your performance. \n\nThe average duration for my screenshot lambda is 10 seconds. Mostly waiting for Chrome to start and webpages to load.\n\nYou pay for every millisecond of that wait. 🙃\n\n## Thinking about scalability\n\n[Scalability](https://en.wikipedia.org/wiki/Scalability) talks about how your system behaves as load grows.\n\nWhat happens at 10 requests per day? What about 10,000,000?\n\nYour system is considered scalable, if resources grow linearly or less with demand. Using 1 lambda per request may not be cheap, but it does scale.\n\nA system that grows faster than demand blows up eventually.\n\n![](../../images/scalability.png)\n\nThis applies to teams as much as it does to computers. If you need 3 new developers to do 2x more work, your company will die.\n\nScaling your software comes in 2 flavors: vertical and horizontal. Use a mix of both.\n\n### Vertical scaling\n\nVertical scaling is the art of getting 1 computational resource to do more. \n\nThis type of scaling can get expensive. You need more resources – faster CPU, more memory, better hardware, a GPU – and lots of engineering effort to optimize your code.\n\nFor many workloads, this approach is best.\n\nIt's easier to scale a database by adding CPU and memory than by rebuilding your application to use more databases. AI researchers prefer a computer with terabytes of memory over tweaking algorithms to need less.\n\nAnd sometimes it's the only way. Like processing a video.\n\n### Horizontal scaling\n\nHorizontal scaling is the art of splitting work between computational resources.\n\nThis type of scaling can be cheap. Easier to provision, quicker to get going, less effort to optimize.\n\nBut you have to find a balance. 6 cheap computers can cost more than 3 expensive computers.\n\nThat's where serverless shines.\n\nHorizontal scaling is perfect for isolated operations with little inter-dependency. Like API requests, processing a video library, or serving static files. \n\nUse the [map-reduce pattern](/lambda-pipelines) to split larger tasks like we did in the Lambda Pipelines chapter. You pay with system complexity.\n\n## Achieving speed\n\nNever optimize your code until it tells you where it hurts. Bottlenecks are surprising and unpredictable. Measure.\n\nBut don't kick the table with your pinky toe either. You already know that hurts.\n\nFor maximum speed in serverless environments focus on:\n\n- cold wake up times\n- reducing input output\n- fast simple code\n\n### Cold wake up times\n\nTraditional servers split performance into warm and cold. The server starts cold and warms up its caches, algorithm setup, and execution environment with the first few requests.\n\nMost requests reach a warm server which is orders of magnitude faster.\n\nWith serverless, every request could be cold.\n\n![](giphy:gulp)\n\nThere is no one solution I can give you. Optimizing cold boot performance takes work and understanding _your_ software.\n\nA few areas to look at:\n\n- Use a language with a **fast and nimble runtime**. JavaScript is surprisingly effective, Go can work great. JVM-based languages tend to struggle.\n- **Ruthlessly reduce bundle size**. The less code that needs to load, the better. Compile and minimize your source, [remove un-needed dependencies](https://www.serverless.com/framework/docs/providers/aws/guide/packaging#package-configuration). Use the `exclude` config in `serverless.yml`.\n- **Avoid fancy algorithms** with large constants. Iterating your data 5x to set up a fast algorithm may not be worth it. You're processing small payloads.\n\n### Reduce input output\n\nThe biggest performance killer is input/output. Your tiny server comes with no batteries included.\n\nWant to read from cache? Network request. Database? Network request. Call an API? Yep, network.\n\n[Networks are slow](https://gist.github.com/hellerbarde/2843375). If reading a variable is like brushing your teeth, a roundtrip with your database is like a 6 day vacation. 🤯\n\n![Latency numbers every programmer should know](https://camo.githubusercontent.com/76ced2beb27b9053184da0cd7bf2d671f901d1521fcb35cb00d55631d22d5b85/687474703a2f2f692e696d6775722e636f6d2f6b307431652e706e67)\n\nProviders optimize these \"external\" requests but you have no control and there are hard limits at play.\n\nBest you can do is minimize.\n\n- do more in 1 request (like bigger SQL queries)\n- avoid I/O in a loop, try to pre-fetch\n- prefer local [memoization](https://en.wikipedia.org/wiki/Memoization) over cache\n\n#### Memoization vs. cache\n\nMemoization stores results in a local variable. Call your function with the same inputs and return the value without recalculating.\n\nBut you lose memory when your lambda goes to bed. That's where a cache can help. A service with persistent memory that stores pre-computed values.\n\nLike your database, the caching service is an external request. Much slower than a local variable.\n\nUse memoization when you call the same code multiple times per request. Use cache when you need the same data across many requests.\n\nWhen you get requests frequently enough, serverless providers reuse your code between executions. Memoizing in \"global\" memory does wonders.\n\nIn pseudo-javascript:\n\n```javascript\nlet memoized = null\n\nexports.handler = function (argument) {\n  if (!memoized) {\n    memoized = expensiveComputation()\n  }\n\n  return memoized\n}\n```\n\n### Fast simple code\n\nKeep it simple.\n\nUse the fastest check as your first argument in an `&&` or `||` chain. Computers skip arguments that can't change the result.\n\nAvoid doing work that you'll throw away 10 lines later.\n\nAn `O(n)` search can be faster than building a hashmap in `O(n)` and reading it once.\n\n## Think of your system as a whole\n\nOnce you've optimized individual components, it's time to look at your system as a whole. Find the bottlenecks.\n\n> The fastest algorithm in the world is worthless when throttled by a slow database. \n\nA bottleneck happens when there's a performance mismatch between parts of your system. Fast code feeding into slow code. Slow code that your whole system relies on ...\n\nTypical offenders include:\n\n- large computation (like video and image processing)\n- poorly optimized databases\n- 3rd party APIs\n\n![Bottlenecks impact your whole system](../../images/spreading-slowness.png)\n\nYou can find the bottleneck by looking at your queues. Is one of them filling up with data? Likely feeding a bottleneck.\n\nYou can look at Lambda execution times. A slow Lambda could be full of bad code, more likely it's talking to a bottleneck. \n\nCan you move it off the [critical path](https://en.wikipedia.org/wiki/Critical_path_method)? Cache or memoize any API and database responses?\n\nThe exact answer depends on *your* code and *your* system.\n\n## Optimizing cost\n\n> Value your time.\n\nI talked to an AWS billing expert and that was the take-away. Then I killed the whole chapter on cost.\n\nYour time is worth more than your bill.\n\nHowever, he suggested you try [AWS Lambda Power Tuning](https://github.com/alexcasalboni/aws-lambda-power-tuning). A tool that tries your lambda in different configurations and shows the balance between power, speed, and cost.\n\n![Example graph from AWS Lambda Power Tuning](../../images/aws-lambda-power-tuning.jpg)\n\nExecution time goes from 35s with 128MB to less than 3s with 1.5GB, while being 14% cheaper to run.\n\nA great example of how vertical scaling beats horizontal. But you keep every benefit of horizontal, because serverless. 🚀\n\nNext chapter we look at running Serverless Chrome, a typical case where beefy lambdas help lots."
  },
  {
    "path": "src/pages/serverless-pros-cons/index.mdx",
    "content": "---\ntitle: \"Serverless Pros & Cons\"\ndescription: \"Serverless isn't for everyone every time. Learn how to make the right decision\"\nimage: \"./img/serverless-pros-cons.png\"\n---\n\n# Serverless Pros & Cons – when should you go serverless?\n\n![](../../images/chapter_headers/serverless-pros-cons.svg)\n\nOkay you've heard of serverless, tried it out, and you think it's neat. But should you *really* go serverless for your next project?\n\nYes!\n\nMost of the time ...\n\n![](giphy:weighing-options)\n\nServerless is a great option for most projects most of the time. You save configuration and maintenance time, gain flexibility, and in extreme cases spend more $$ per request than building your own servers.\n\nLarge apps can reach the cost curve limits of serverless. Bank of America, for example, [announced $2B in savings](https://www.businessinsider.com/bank-of-americas-350-million-internal-cloud-bet-striking-payoff-2019-10) from building their own data centers.\n\nYou won't hit those issues. And if you do, I hope there's a business model to back it up and you can afford DevOps professionals. 😛\n\nLarge orgs tend to provide a cloud or serverless-like environment internally. If you have access to that, use it.\n\n## Serverless is an ecosystem\n\nWhen I say serverless, I don't mean just throwing up code on a [function-as-a-service](https://en.wikipedia.org/wiki/Function_as_a_service) platform like AWS Lambda. I'm talking about the whole ecosystem.\n\nThe core idea is this:\n\n1. Backend does as little as possible\n2. Clients tie everything together\n3. Static files come from fast [content delivery networks](https://en.wikipedia.org/wiki/Content_delivery_network)\n4. Database handles data consistency\n5. As much work as possible happens at compile and deploy time\n\nUsers' and developers' machines do the hard work.\n\nIs part of your app the same for every user? Package it up at deploy time. No need to bother the server *or* the client with that work.\n\nIs part of your app specific to individual users? Let the client handle it. Every phone is a powerful computer these days.\n\nGot dynamic data that needs to synchronize across multiple sessions or users? Background processes that aren't tied to a specific session? Perfect fit for your server and database.\n\nWe go in depth about this architecture in the chapter on [Serverless Architecture Principles](/serverless-architecture-principles).\n\n## Serverless pros\n\nThe main benefit of serverless is that you don't deal with servers. They're somebody else's problem.\n\n### You save time\n\nYou focus on *application* code. No more tedious maintenance tasks that aren't specific to your problem. \n\nBye bye yak shaving. 👋\n\n![](giphy:shave_yak)\n\n*\"I need an API. That means I have to run a server. Which means I need Apache or Nginx to map HTTP requests to my app server. I need a computer to run all that. Which means I have to set up a whole operating system. Then I have to make sure everything runs at boot. And if a process goes down, it needs to restart. And ...\"*\n\nAfter all that work you get to build your application.\n\nWith serverless **you save time otherwise spent managing servers.** Whether that's you personally or a DevOps team in your organization. \n\n### Programming productivity\n\nYou write backend code more productively.\n\n<div id=\"lock\" />\n\nSmaller, more self-contained code, ideally a single function, brings clarity and focus. [Do one thing and do it well](https://en.wikipedia.org/wiki/Unix_philosophy#Do_One_Thing_and_Do_It_Well)\n\nWith increased focus, you get:\n\n- easier testing\n- quicker understanding\n- shorter development cycles\n\n### Often cheaper\n\nServerless can be cheaper to run. \n\nYou save opportunity and employee cost *and* you're not paying for servers you aren't using.\n\nAs mentioned in the [Getting Started](/getting-started) chapter: before serverless, you'd have to (over)provision a bunch of machines in case there's a traffic spike. That means you're paying for servers you aren't using.\n\nWith serverless, you pay per execution and run time. Like pay-as-you-go pricing: Run code, pay for that run.\n\n**When there's no traffic, there's no cost. 👌**\n\n### Scalability\n\nGoogle likes to call serverless architectures *‌from prototype to production to planet-scale*. You don't want to use serverless at planet scale though.\n\nBut Google is right: Serverless scales. A lot.\n\nThe details on *why* serverless is scalable are tricky to get into. It has to do with how much work you can pack into a single physical machine ... but there *is* a machine somewhere and you might run out of those with truly planet-scale work 🤔\n\nIt comes down to this: You're running a [hyper-elastic server](https://en.wikipedia.org/wiki/Elasticity_(cloud_computing)) that adapts to changes in workload at millisecond precision. In theory this gives you perfect utilization.\n\n## Serverless cons\n\nAs much as I think serverless is the next big thing in web development, it's not all fun and games out there. There *are* disadvantages to using serverless.\n\n### Higher latency for low workloads\n\nPerformance comes in two flavors:\n\n1. Latency\n2. Speed or bandwidth\n\nLatency talks about how long it takes from making a request to getting a response. Speed talks about how long it takes to do work.\n\nEach *execution* is fast because the code is small and servers are fast. A few milliseconds and you're done.\n\nBut *latency* can be high. You're hitting the server cold every time. That means each request waits for the computer to wake up.\n\nThat's why providers keep servers live between requests. But only if requests come often enough.\n\n**For low traffic applications with low latency demands, you might need a constantly provisioned server.**\n\n### Sometimes costly\n\nAs [Bank of America found out](https://www.businessinsider.com/bank-of-americas-350-million-internal-cloud-bet-striking-payoff-2019-10) pay-as-you-go pricing gets expensive when used a lot.\n\nServerless providers charge based on number of requests and resources used. You pay for every request and every millisecond of computation. Known as \"compute\".\n\nIf you have a lot of requests or long runtimes, you can rack up the costs beyond what you'd pay with your own servers.\n\nFor example: You wouldn't want to train a machine learning model on a serverless architecture. Learned that painful lesson with my first startup in 2010 and GoogleAppEngine. Flicked the On switch and the credit card melted 🔥\n\nAnother bad case for serverless are high traffic applications. At millions of requests per second, you're better off on your own.\n\n**Serverless becomes expensive at high loads**. Where the balance tips depends on what you're doing, how much time you're saving, and how much it costs to do yourself.\n\n### Vendor lock-in\n\nThis one's simple: You're building on somebody else's infrastructure. \n\nIf that infrastructure changes, you're screwed. If they crank up the price, you're screwed. If you want to move, you're screwed. If you want to deploy your own, you're screwed.\n\nYou *can* do all those things, but it's a tedious and difficult task that might break your app. You're not building features or working on your business while you migrate.\n\nStartups rarely live long enough to have this problem. Enterprises take defensive measures like multi-year contracts with strict service level agreements. \n\n**Avoid building architecture agnostic code.** It's hard and you're not likely to need it.\n\n### Systems complexity\n\nYou're paying for the simplicity of your application code with system complexity. Individual functions are simpler and easier to test. Complexity comes from how they interact.\n\nWe'll talk more about that in the [Robust Backend Design](/robust-backend-design) chapter.\n\n## The verdict?\n\n![](giphy:the_verdict)\n\nIt depends. You will have to think about this yourself :)\n\n[Ping me on twitter](https://twitter.com/swizec), if you'd like some help.\n\nI like to use a series of questions:\n\n- *\"Will this require a lot of computation?* if the answer is yes, I consider building my own servers.\n- *\"Will this have ridiculously high traffic?* if the answer is yes, I'd choose serverless because I hate doing DevOps. High traffic hopefully means I can afford a professional :)\n- *\"Is this a small side project idea?\"* serverless all the way\n- *\"Does every request need to be served under 10ms?\"* you're gonna have to roll your own\n\nNext chapter, we're going to talk about different serverless providers."
  },
  {
    "path": "src/pages/serverless-rest-api/index.mdx",
    "content": "---\ntitle: \"Creating a serverless REST API\"\ndescription: \"What is REST and how does it work? How do you build a serverless API?\"\nimage: \"./img/serverless-rest-api.png\"\n---\n\nimport { TestCloudFunction } from \"../../components/TestCloudFunctions.js\"\n\n# Creating a REST API\n\n![](../../images/chapter_headers/serverless-rest-api.svg)\n\nYou've heard of REST before, but what **_is_** REST? How does it work? Can you build one from scratch? Does serverless make your REST life easier?\n\nIn this chapter, learn about REST best practices and finish with a small implementation you can try right now. I left mine running ✌️\n\n## What is REST\n\n[REST](https://en.wikipedia.org/wiki/Representational_state_transfer) stands for REpresentational State Transfer. Coined in [Roy Fielding's 2000 doctoral thesis](https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm), it now represents the standard approach to web APIs.\n\nFielding says REST is an architectural principle. It _recommends_ how to build a scalable web system, but there's no official standard.\n\nYou may have noticed this in the wild. RESTful APIs follow similar guidelines and no two are alike.\n\n**These days any API that uses HTTP to transfer data and URLs to identify resources is called REST.**\n\nHere are the 6 architectural constraints that define a RESTful system 👇\n\n- **client-server architecture** specifies a separation of concerns between the user interface and the storage of data. This simplifies both sides and lets them evolve independently.\n- **statelessness** specifies that _the protocol_ is stateless. Each request to the server contains all information necessary to process that request. Servers do not maintain client context.\n- **cacheability** specifies that clients can cache any server response to improve performance on future requests. Servers have to annotate responses with appropriate caching policies via http headers\n- **layered system** means that, like regular HTTP, a RESTful client shouldn't need to know whether it's talking to a server, a proxy, or load balancer. This improves scalability.\n- **code on demand (optional)** says that servers can send executable code as part of their responses. You see this with web servers sending JavaScript.\n- **uniform interface** is the most fundamental and means that clients can interact with a server purely from responses, without outside knowledge.\n\n### A uniform interface\n\nCreating a uniform interface is the most important aspect of a RESTful API. The less clients know about your server, the better.\n\nEach **request identifies the resource** it is requesting. Using the URL itself.\n\n**Responses send a representation** of a resource rather than the resource itself. Like compiling a set of database objects into a JSON document.\n\nAll **messages are self-descriptive** meaning both client and server can understand a message without external information. Send everything you need.\n\nA resource's **representation includes everything needed to modify** that resource. When clients get a response, it should contain everything necessary to modify or delete the underlying resources.\n\nAcademics say **responses should list possible actions** so clients can navigate a system without intimate knowledge of its API. You rarely see this in the wild.\n\n## Designing a RESTful API\n\nThe trickiest part of building a RESTful API is how it evolves over time. The second trickiest is keeping it consistent across your app.\n\nAs your system grows you're tempted to piggy-back on existing APIs and break resource constraints. You're likely to forget the exact wording and phrasing of different parts.\n\nAll that is natural. **The important part is to start on the right foot and clean up when you can.**\n\n### Engineers are human\n\nWhen engineers _can_ do something with your API, they will.\n\nThey're going to find every undocumented feature, discover every \"alternative\" way to get data and uncover any easter egg you didn't mean to include. They're good at their job.\n\nDo yourself a favor and aim to **keep everything consistent**. The more consistent you are, the easier it will be to clean up.\n\nThat means\n\n- all dates in the same format\n- all responses in the same shape\n- keep field types consistent, if an error is a string it's always a string\n- include every field name and value in full\n- when multiple endpoints include the same model, make it _the same_\n\nHere are tips I've picked up over the past 14 years of building and using RESTful APIs.\n\n### URL schema\n\nYour URL schema exists to solve _one_ problem: Create a uniform way to identify resources and endpoints on your server.\n\nKeep it consistent, otherwise don't sweat it.\n\nEngineers like to get stuck on pointless details, but it's okay. Make sure your team agrees on what makes sense.\n\nWhen designing a URL schema I aim to:\n\n- **make it guessable**, which stems from consistency and predictability\n- **make it human readable**, which makes it easier to use, debug, and memorize\n- **avoid query strings** because they look messy and can make copy paste debugging harder\n- **avoid strange characters** like spaces and emojis. It looks cute, but it's cumbersome to work with\n- **include IDs in URLs** because it makes debugging and logging easier\n\nThe two schemas I like, go like this:\n\n```\nhttps://api.wonderfulservice.com/<namespace>/<model>/<id>\n```\n\nUsing an `api.*` subdomain helps with load balancing and using special servers for your API. Less relevant in the serverless world because providers create unique domains.\n\nThe optional `<namespace>` helps you stay organized. As your app grows, you'll notice different areas use similar-sounding names.\n\nYou don't need a namespace for generic models like `user` or `subscription`.\n\nAdding a `<model>` that's named after the model on your backend helps you stay sane. Yes it breaks the idea of total separation between client and server, but it's useful to maintain consistency.\n\n**If everyone calls everything the same name, you never need to translate. 😉**\n\nThe `<id>` identifies the specific instance of a model that you're changing.\n\nSometimes it's useful to use this alternative schema:\n\n```\nhttps://api.wonderfulservice.com/<namespace>/<model>/<verb>/<id>\n```\n\nThe verb specifies what you're doing to the model. More on that when we discuss [HTTP verbs](#using-http-verbs) further down.\n\n### Data format\n\nUse JSON. Both for sending data and for receiving data.\n\n<div id=\"lock\" />\n\nGreat support in common programming languages, easy to use with JavaScript, simple to write by hand, readable by humans, not verbose. That makes JSON the perfect format for a modern API.\n\n**For timestamps** I recommend the [ISO 8601 standard](https://en.wikipedia.org/wiki/ISO_8601) for the same reason. Great tooling in all languages, readable by humans, writable by hand.\n\n[UNIX timestamps](https://en.wikipedia.org/wiki/Unix_time) used to be popular and are falling out of favor. Hard to read for humans, don't work great with dates beyond 2038, bad at dates before 1970.\n\nStick to ISO time and JSON data 😊\n\n### Using HTTP verbs\n\nVerbs specify _what_ your request does with a resource.\n\nOpinions on how to pass verbs from client to server vary. There's 3 camps:\n\n1. stick to HTTP verbs\n2. verbs belong in the URL\n3. verbs are part of the JSON payload\n\nEveryone agrees that a `GET` request is for getting data and should have no side-effects.\n\nOther verbs belong to the `POST` request. Or you can use HTTP verbs like `PUT`, `PATCH`, and `DELETE`.\n\nI like to use a combination.\n\n`GET` for getting data. `POST` for posting data (both create and update). `DELETE` for deleting ... on the rare occasion I let clients delete data.\n\n`PUT` and `PATCH` can be frustrating to work with in client libraries.\n\n### Errors\n\nShould you use HTTP error codes to communicate errors?\n\nOpinions vary.\n\nOne camp says that HTTP errors are for HTTP-layer problems. Your server being down, a bad URL, invalid payload, etc. When your application processes a request and decides there's an error, it should return 200 with an error object.\n\nThe other camp says that's silly and we already have a great system for errors. Your application should use the full gamut of HTTP error codes _and return an error object_.\n\n**Always return a descriptive JSON error object.** That's a given. Make sure your server doesn't return HTML when there's an error.\n\nAn error shape like:\n\n```json\n{\n  \"status\": \"error\",\n  \"error\": \"This is what went wrong\"\n}\n```\n\nis best.\n\nI like to combine both approaches. `500` errors for my application failing to do its job, `404` for things that aren't found, `200` with an explanation for everything else. Helps avoid hunting for obscure error codes and sticks to the basics of HTTP.\n\nAdded bonus: You can read the error object. Will you remember what error 418 means?\n\n### Versioning\n\nI have given up on versioning APIs.\n\nYour best bet is to maintain eternal backwards compatibility. Make additive changes when needed, remove things never.\n\nIt leads to messy APIs, which is a shame, but better than crashing an app the user hasn't updated in 3 years.\n\nWhen you need a breaking change, it's unlikely the underlying model fits your current URL schema. Use a new URL.\n\n## Build a simple REST\n\nTo show you how this works in practice, we're going to build a serverless REST API. Accepts and returns JSON blobs, stores them in DynamoDB.\n\nYou can [see the full code on GitHub](https://github.com/Swizec/serverlesshandbook.dev/tree/master/examples/serverless-rest-example). I encourage you to play around and try deploying to your own AWS.\n\nCode samples here are excerpts.\n\n### URL schema mapped to Lambdas\n\nWe start with a URL schema and lambda function definitions in `serverless.yml`.\n\n```yaml\n# serverless.yml\n\nfunctions:\n  getItems:\n    handler: dist/manageItems.getItem\n    events:\n      - http:\n          path: item/{itemId}\n          method: GET\n          cors: true\n  updateItems:\n    handler: dist/manageItems.updateItem\n    events:\n      - http:\n          path: item\n          method: POST\n          cors: true\n      - http:\n          path: item/{itemId}\n          method: POST\n          cors: true\n  deleteItems:\n    handler: dist/manageItems.deleteItem\n    events:\n      - http:\n          path: item/{itemId}\n          method: DELETE\n          cors: true\n```\n\nThis defines the 4 operations of a basic CRUD app:\n\n- `getItem` to fetch items\n- `updateItem` to create items\n- `updateItem/id` to update items\n- `deleteItem` to delete items\n\nThe `{itemId}` syntax lets APIGateway parse the URL for us and pass identifiers to our code as parameters.\n\nMapping every operation to its own lambda means you don't have to write routing code. When a lambda gets called, it knows what to do.\n\nWe define lambdas in a [`manageItems.ts` file](https://github.com/Swizec/serverlesshandbook.dev/blob/master/examples/serverless-rest-example/src/manageItems.ts). Grouping operations from the same model helps keep your code organized.\n\n### getItem\n\n```typescript\n// src/manageItems.ts\n\n// fetch using /item/ID\nexport const getItem = async (event: APIGatewayEvent): Promise<APIResponse> => {\n  const itemId = event.pathParameters ? event.pathParameters.itemId : \"\"\n\n  const item = await db.getItem({\n    TableName: process.env.ITEM_TABLE!,\n    Key: { itemId },\n  })\n\n  if (item.Item) {\n    return response(200, {\n      status: \"success\",\n      item: item.Item,\n    })\n  } else {\n    return response(404, {\n      status: \"error\",\n      error: \"Item not found\",\n    })\n  }\n}\n```\n\nThe `getItem` function is triggered from an APIGatewayEvent. It includes a `pathParameters` object that contains an `itemId` parsed from the URL.\n\nWe then use a [DynamoDB `getItem` wrapper](https://github.com/Swizec/serverlesshandbook.dev/blob/master/examples/serverless-rest-example/src/dynamodb.ts#L80) to talk to the database and find the requested item.\n\nIf the item is found, we return a success response, otherwise an error. The `response` method is a helper that makes responses easier to write.\n\n```typescript\n// src/manageItems.ts\n\nfunction response(statusCode: number, body: any) {\n  return {\n    statusCode,\n    body: JSON.stringify(body),\n  }\n}\n```\n\n**You can try it out here:**\n\n<TestCloudFunction\n  serviceName=\"serverless-rest-example\"\n  urlPlaceholder=\"https://4sklrwb1jg.execute-api.us-east-1.amazonaws.com/dev/item/0769413a-b306-46cb-a03c-5a8d5e29aa3e\"\n  defaults\n/>\n\nOr paste that URL into a browser.\n\n### updateItem\n\nUpdating an item is the most complex operation we've got.\n\nIt handles both creating and updating items, always assuming that the client knows best. You'll want to validate that in production.\n\nHere we overwrite server data with the client payload.\n\n```typescript\n// upsert an item\n// /item or /item/ID\nexport const updateItem = async (\n  event: APIGatewayEvent\n): Promise<APIResponse> => {\n  let itemId = event.pathParameters ? event.pathParameters.itemId : uuidv4()\n\n  let createdAt = new Date().toISOString()\n\n  if (event.pathParameters && event.pathParameters.itemId) {\n    // find item if exists\n    const find = await db.getItem({\n      TableName: process.env.ITEM_TABLE!,\n      Key: { itemId },\n    })\n    if (find.Item) {\n      // save createdAt so we don't overwrite on update\n      createdAt = find.Item.createdAt\n    } else {\n      return response(404, {\n        status: \"error\",\n        error: \"Item not found\",\n      })\n    }\n  }\n\n  if (!event.body) {\n    return response(400, {\n      status: \"error\",\n      error: \"Provide a JSON body\",\n    })\n  }\n\n  let body = JSON.parse(event.body)\n\n  if (body.itemId) {\n    // this will confuse DynamoDB, you can't update the key\n    delete body.itemId\n  }\n\n  const item = await db.updateItem({\n    TableName: process.env.ITEM_TABLE!,\n    Key: { itemId },\n    UpdateExpression: `SET ${db.buildExpression(\n      body\n    )}, createdAt = :createdAt, lastUpdatedAt = :lastUpdatedAt`,\n    ExpressionAttributeValues: {\n      ...db.buildAttributes(body),\n      \":createdAt\": createdAt,\n      \":lastUpdatedAt\": new Date().toISOString(),\n    },\n    ReturnValues: \"ALL_NEW\",\n  })\n\n  return response(200, {\n    status: \"success\",\n    item: item.Attributes,\n  })\n}\n```\n\nThe pattern we're following is:\n\n- if no ID provided, generate one\n- find item in DB\n- update or create a `createdAt` timestamp\n- parse the JSON body\n- create an update expression\n- add a `lastUpdatedAt` timestamp\n- return the resulting object\n\nAdding `createdAt` and `lastUpdatedAt` timestamps is helpful when debugging these systems. You want to know when something happened.\n\nYou can find the `db.*` helper methods in my [`dynamodb.ts` file](https://github.com/Swizec/serverlesshandbook.dev/blob/master/examples/serverless-rest-example/src/dynamodb.ts)\n\n**You can try it out here:**\n\nCreate an item 👇\n\n<TestCloudFunction\n  serviceName=\"serverless-rest-example\"\n  urlPlaceholder=\"https://4sklrwb1jg.execute-api.us-east-1.amazonaws.com/dev/item\"\n  jsonPlaceholder={`{\"hello_world\": \"this is a json item\", \"params\": \"It can have multiple params\", \"customNumber\": 4}`}\n  defaults\n/>\n\nUpdate an item, try using the ID from before 👇\n\n<TestCloudFunction\n  serviceName=\"serverless-rest-example\"\n  urlPlaceholder=\"https://4sklrwb1jg.execute-api.us-east-1.amazonaws.com/dev/item/e76e46f4-296d-4fef-a349-a1bb5717f2ac\"\n  jsonPlaceholder={`{\"hello_world\": \"this is a json item\", \"params\": \"It can have multiple params\", \"customNumber\": 4}`}\n  defaults\n/>\n\n### deleteItem\n\nDeleting is easy by comparison. Get the ID, delete the item. With no verification that you should or shouldn't be able to.\n\n```typescript\n// src/manageItems.ts\n\nexport const deleteItem = async (\n  event: APIGatewayEvent\n): Promise<APIResponse> => {\n  const itemId = event.pathParameters ? event.pathParameters.itemId : null\n\n  if (!itemId) {\n    return response(400, {\n      status: \"error\",\n      error: \"Provide an itemId\",\n    })\n  }\n\n  // DynamoDB handles deleting already deleted values, no error :)\n  const item = await db.deleteItem({\n    TableName: process.env.ITEM_TABLE!,\n    Key: { itemId },\n    ReturnValues: \"ALL_OLD\",\n  })\n\n  return response(200, {\n    status: \"success\",\n    itemWas: item.Attributes,\n  })\n}\n```\n\nWe get `itemId` from the URL props and call a `deleteItem` method on DynamoDB. The API returns the item as it was before deletion.\n\n_PS: in a production system you'll want to use soft deletes – mark as deleted and keep the data_\n\n## Fin ✌️\n\nAnd that's 14 years of REST API experience condensed into 2000 words. My favorite is how much easier this is to implement using serverless than with Rails or Express.\n\nI love that you can have different lambda functions (whole servers, really) for individual endpoints.\n\nNext chapter, we look at GraphQL and why it represents an exciting future.\n"
  },
  {
    "path": "src/pages/thanks.mdx",
    "content": "export const title = \"Thanks ❤️\"\n\nexport const description = \"Thanks for supporting the Serverless Handbook\"\n\nimport { Box } from \"theme-ui\"\nimport { TypeformLink } from \"@swizec/gatsby-theme-course-platform\"\n\n# Thank you for supporting Serverless Handbook ❤️\n\n<lite-youtube videoid=\"cgmjsd9biyE\" autoload></lite-youtube>\n\nHey thanks again for supporting Serverless Handbook. I hope it helps you on your journey.\n\nHere's what happens next 👇\n\nI just saved a cookie in your browser. Now it knows that you have full access to the content. No more opt-in buttons, hidden content, or anything like that. Get started with the menu on the left, or click the Back button to see the page you were looking at before.\n\nMy robots created an account for you and sent an email with further instructions. **You'll have to set a password.**\n\nOver the next few weeks, you'll get a chapter in your inbox every 3 days or so. Because buying books and reading books are different hobbies :)\n\nWanna help improve Serverless Handbook? Answer 2 quick questions\n\n<TypeformLink url=\"https://swizecteller.typeform.com/to/AJgFM5\" />\n\nHappy learning,<br/>\n~Swizec\n"
  },
  {
    "path": "static/_redirects",
    "content": "/serverless-cost    /serverless-performance#optimizing-cost"
  }
]