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 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 existence check
GET /users/checkMembership/{email}usersMembership flag check
GET /profiles/profile/{profileID}profilesPublic profile view
POST /payments/webhookpaymentsStripe webhook (signed)
GET /eventseventsEvent listing
GET /events/{id}/{year}eventsSingle event
GET /events/getActiveEventeventsNext upcoming event
POST /registrationsregistrationsRegister for event

Admin-only endpoints additionally check the email domain in handler code:


Local Development

How index.js Works

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

  1. Reads sls-multi-gateways.yml to get the service list and base port (4001)
  2. Spawns each service with sls offline starting at port 4001, incrementing by one per service
  3. Runs a lightweight Express proxy on port 4000 that matches URL path prefixes to the correct service port

Each service's path prefix comes from the srvPath field in sls-multi-gateways.yml. For example, the events service has srvPath: events, so /events/* requests are routed to it.

DynamoDB Local

DynamoDB Local starts automatically when sls offline starts for any service that has the serverless-dynamodb plugin configured. It runs on port 8000. Most services configure it via their serverless.yml with noStart: true (they reuse the same instance).

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. With REST API Gateway (which is what this backend uses), claims are accessed via authorizer.claims:

const email = event.requestContext?.authorizer?.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:
      - http:
          path: /my-service
          method: get
          cors: true
          authorizer:
            type: COGNITO_USER_POOLS
  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 runs locally and deploys with everything else:
- srvName: my-service
  srvPath: my-service
  srvSource: services/my-service

The proxy automatically assigns the next available port starting from the base port in the config. No manual port assignment is needed.

Previous
Shared Libraries