Backend Guide

The backend consists of AWS Lambda functions managed by Serverless Framework 3, located at platform/backend/.

Project Structure

platform/backend/
├── serverless.yml          # Infrastructure and function definitions
├── package.json
├── src/
│   ├── handlers/           # Lambda handler entry points
│   │   ├── getPresignedUrl.js
│   │   ├── submitIntake.js
│   │   ├── processSubmission.js
│   │   ├── documentAnalyzer.js
│   │   ├── quoteCalculator.js
│   │   ├── pipedriveClient.js
│   │   ├── getSubmission.js
│   │   ├── tokenGenerator.js
│   │   └── webhookHandler.js
│   └── lib/                # Shared utilities
│       ├── dynamodb.js
│       ├── s3.js
│       └── ssm.js
└── test/
    └── events/             # Test event JSON files for local invocation

serverless.yml Structure

The serverless.yml file defines the service, provider configuration, functions, and resources.

Provider Section

service: dissertation-editor-backend

provider:
  name: aws
  runtime: nodejs20.x
  region: us-east-1
  stage: dev
  environment:
    SUBMISSIONS_TABLE: submissions-${self:provider.stage}
    CONFIG_TABLE: config-${self:provider.stage}
    UPLOAD_BUCKET: dissertation-editor-uploads-${self:provider.stage}
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
            - dynamodb:Query
          Resource:
            - arn:aws:dynamodb:${self:provider.region}:*:table/submissions-*
            - arn:aws:dynamodb:${self:provider.region}:*:table/config-*
        - Effect: Allow
          Action:
            - s3:GetObject
            - s3:PutObject
          Resource: arn:aws:s3:::dissertation-editor-uploads-*/*
        - Effect: Allow
          Action:
            - ssm:GetParameter
          Resource: arn:aws:ssm:${self:provider.region}:*:parameter/dissertation-editor/*
        - Effect: Allow
          Action:
            - lambda:InvokeFunction
          Resource: arn:aws:lambda:${self:provider.region}:*:function:${self:service}-*

Function Definition Pattern

Each function maps to an HTTP event on API Gateway:

functions:
  submitIntake:
    handler: src/handlers/submitIntake.handler
    timeout: 10
    memorySize: 256
    events:
      - http:
          path: intake/submit
          method: post
          cors: true

For functions that are invoked asynchronously by other Lambdas (not by API Gateway), omit the events section:

  documentAnalyzer:
    handler: src/handlers/documentAnalyzer.handler
    timeout: 30
    memorySize: 512

Lambda Handler Pattern

All handlers follow this pattern:

const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, PutCommand } = require('@aws-sdk/lib-dynamodb');

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

module.exports.handler = async (event) => {
  try {
    const body = JSON.parse(event.body || '{}');

    // Business logic here

    return {
      statusCode: 200,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ /* response data */ }),
    };
  } catch (error) {
    console.error('Error:', error);
    return {
      statusCode: 500,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ error: 'Internal server error' }),
    };
  }
};

Key conventions:

  • CORS headers are included in every response (API Gateway CORS config handles OPTIONS, but Lambda must set headers on actual responses).
  • event.body is always parsed from JSON string.
  • Errors are caught and returned as 500 with a generic message. Detailed errors go to console.error which writes to CloudWatch.
  • SDK clients are instantiated outside the handler for connection reuse across warm invocations.

Async Invocation Pattern

submitIntake invokes processSubmission asynchronously so the user gets an immediate response:

const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda');

const lambdaClient = new LambdaClient({});

// Inside the handler:
await lambdaClient.send(new InvokeCommand({
  FunctionName: process.env.PROCESS_SUBMISSION_FUNCTION,
  InvocationType: 'Event',  // async -- returns immediately
  Payload: JSON.stringify({
    submissionId,
    s3Key,
  }),
}));

The function name is passed via environment variable in serverless.yml:

  submitIntake:
    handler: src/handlers/submitIntake.handler
    environment:
      PROCESS_SUBMISSION_FUNCTION: ${self:service}-${self:provider.stage}-processSubmission

Adding a New Function

  1. Create the handler file in src/handlers/:

    // src/handlers/myNewFunction.js
    module.exports.handler = async (event) => {
      // ...
    };
    
  2. Add the function to serverless.yml:

    functions:
      myNewFunction:
        handler: src/handlers/myNewFunction.handler
        timeout: 10
        memorySize: 256
        events:
          - http:
              path: my/endpoint
              method: get
              cors: true
    
  3. If the function needs additional IAM permissions beyond what the shared role provides, add them to the provider IAM statements.

  4. Deploy:

    AWS_SDK_LOAD_CONFIG=1 npx serverless deploy --aws-profile dissertation-editor
    

IAM Permissions

The shared IAM role grants all functions access to:

  • DynamoDB: GetItem, PutItem, UpdateItem, Query on submissions and config tables
  • S3: GetObject, PutObject on the uploads bucket
  • SSM: GetParameter on /dissertation-editor/* parameters
  • Lambda: InvokeFunction on all functions in this service (for async invocation)

If a function needs permissions to a resource not listed above (e.g., SES for email), add a new IAM statement in the provider section.