Backend

Services & Patterns

How the backend environment works, how services share an API Gateway, how local development runs, and the patterns you should follow when writing code.


Environment Configuration

Three Environments

EnvironmentAPI DomainConfig FileENVIRONMENT var
Devapi-dev.ubcbiztech.comconfig.dev.json"" (empty)
Stagingapi-staging.ubcbiztech.comconfig.staging.json"" (empty)
Productionapi.ubcbiztech.comconfig.prod.json"PROD"

Key Difference

The ENVIRONMENT suffix on table names is the only difference between dev/staging and production data. Both dev and staging use the same base table names (which means they share data).

Dev and Staging Share Data

Because both dev and staging have ENVIRONMENT="", they read from and write to the same DynamoDB tables. Be careful when testing destructive operations in staging.


API Gateway Architecture

Shared API Gateway

The hello service creates and exports the API Gateway. All other services import it:

hello service (port 4001)
  ├── Creates REST API Gateway
  ├── Creates Cognito Authorizer
  └── Exports both via CloudFormation

All other services
  └── Import the shared Gateway + Authorizer via CloudFormation references

This means all 19 services share a single API Gateway and Cognito authorizer, appearing as one unified API.

Authorization

Most endpoints require a Cognito JWT token. Some endpoints are public (no auth):

Public EndpointsServiceWhy
GET /hellohelloHealth check
GET /users/check/{email}usersPre-login check
GET /users/isMember/{email}usersMembership check
GET /profiles/profile/{profileID}profilesPublic profile view
POST /payments/webhookpaymentsStripe webhook
POST /discord/*botsDiscord webhook
GET /events/nearesteventsUpcoming event
GET /quests/kiosk/*questsKiosk display

Admin-only endpoints additionally check the email domain:

const email = event.headers.authorization; // Decoded from Cognito JWT
if (!email.includes("@ubcbiztech.com")) {
  return helpers.createResponse(403, "Unauthorized - admin access required");
}

Local Development

How index.js Works

When you run npm start, the entry point (index.js) does the following:

  1. Starts DynamoDB Local on port 8000
  2. Spawns each service with serverless offline on ports 4001–4019
  3. Runs a lightweight HTTP proxy on port 4000 that:
    • Receives all requests
    • Matches the URL path to the correct service
    • Forwards the request to the service's port
    • Returns the response

This simulates the production API Gateway routing behavior locally.

Testing

# Run all unit tests
npm test

# Run tests for a specific service
cd services/events && npx mocha test/

# Run integration tests
bash scripts/run_itests.sh

Tests use Mocha + Chai + Sinon for assertions, mocking, and stubbing.


Code Patterns & Conventions

Handler Pattern

Every handler follows this structure:

export const myHandler = async (event) => {
  try {
    // 1. Parse input
    const body = JSON.parse(event.body || "{}");
    const { id } = event.pathParameters || {};

    // 2. Validate
    const error = helpers.checkPayloadProps(body, {
      name: "string",
      year: "number",
    });
    if (error) return helpers.inputError(error);

    // 3. Business logic
    const result = await db.create({ id, ...body }, TABLE_NAME);

    // 4. Return success
    return helpers.createResponse(201, result);
  } catch (err) {
    console.error(err);
    return helpers.createResponse(err.statusCode || 502, err.message);
  }
};

Error Handling

Use the helper response functions for consistent error responses. Don't throw raw errors. Catch them and return structured responses:

// ✅ Good
if (!user) return helpers.notFoundResponse("User");

// ❌ Bad
if (!user) throw new Error("User not found");

Module System

The backend uses ES Modules (not CommonJS). Files use import/export syntax:

// ✅ ES Modules
import db from "../../lib/db.js";
export const handler = async (event) => { ... };

// ❌ CommonJS (don't use)
const db = require("../../lib/db.js");
module.exports.handler = async (event) => { ... };

File Extensions Required

With ES Modules, you must include .js in import paths: import db from "../../lib/db.js", not import db from "../../lib/db".

Admin Checks

Services that need admin access check the email from the Cognito JWT:

const email = event.requestContext?.authorizer?.jwt?.claims?.email;
if (!email || !email.includes("@ubcbiztech.com")) {
  return helpers.createResponse(403, "Admin access required");
}

Adding a New Service

  1. Create the service directory:
mkdir services/my-service
  1. Create serverless.yml:
service: biztechapp-my-service
frameworkVersion: "3"

custom: ${file(../../serverless.common.yml):custom}
provider: ${file(../../serverless.common.yml):provider}
plugins: ${file(../../serverless.common.yml):plugins}

functions:
  myServiceGet:
    handler: handler.get
    events:
      - httpApi:
          path: /my-service
          method: get
          authorizer:
            name: cognitoAuthorizer
  1. Create handler.js:
import helpers from "../../lib/handlerHelpers.js";
import db from "../../lib/db.js";

export const get = async (event) => {
  try {
    // Your logic here
    return helpers.createResponse(200, { message: "Success" });
  } catch (err) {
    console.error(err);
    return helpers.createResponse(502, err.message);
  }
};
  1. Add to sls-multi-gateways.yml so it deploys with everything else.

  2. Add to index.js for local development (assign a port).

Previous
Shared Libraries