A serverless application for creating and managing surveys, built with AWS CDK and TypeScript.
The application uses the following AWS services:
- Amazon DynamoDB - for storing surveys and responses
- AWS Lambda - for serverless API handlers
- Amazon API Gateway - for the REST API
- Amazon Cognito - for authentication (optional, supports local API Key authentication)
- AWS CDK - for infrastructure as code
The application uses Dynamoose as an ORM for DynamoDB with the following models:
// Survey model for storing survey information
interface Survey {
surveyId: string; // Primary key
title: string; // Survey title
description?: string; // Optional description
questions: Question[]; // Array of question objects
ownerId: string; // User ID of survey creator
createdAt: string; // Creation timestamp
updatedAt: string; // Last update timestamp
isActive: boolean; // Whether survey is accepting responses
isPublic: boolean; // Whether survey is publicly viewable
responseCount: number; // Number of responses received
settings?: SurveySettings; // Optional survey settings
}
The application supports various question types:
enum QuestionType {
TEXT = 'text', // Free text response
MULTIPLE_CHOICE = 'multiple_choice', // Select one option
CHECKBOX = 'checkbox', // Select multiple options
RATING = 'rating', // Numeric rating (e.g., 1-5)
DATE = 'date', // Date selection
EMAIL = 'email', // Email input
NUMBER = 'number', // Numeric input
}
Customization options for surveys:
interface SurveySettings {
allowAnonymous?: boolean; // Allow responses without authentication
showProgressBar?: boolean; // Show progress during survey
showQuestionNumbers?: boolean; // Show question numbers
confirmationMessage?: string; // Message shown after submission
redirectUrl?: string; // URL to redirect after completion
theme?: string; // Visual theme
customCss?: string; // Custom styling
}
// Response model for storing survey responses
interface Response {
responseId: string; // Primary key
surveyId: string; // Survey ID (indexed for queries)
answers: Answer[]; // Array of answer objects
respondentId?: string; // Optional user ID of respondent
submittedAt: string; // Submission timestamp
metadata?: object; // Optional metadata
ipAddress?: string; // IP address of respondent
userAgent?: string; // User agent of respondent
}
// Answer structure
interface Answer {
questionId: string; // ID of the question
value: string | string[]; // Answer value (string or array for multi-select)
}
You can run the app locally using LocalStack, which simulates AWS services on your local machine.
- Node.js and npm
- Docker and Docker Compose
- AWS CLI
- Install dependencies:
npm install
- Start LocalStack:
npm run localstack:start
- Deploy the app to LocalStack:
npm run localstack:deploy
- Add sample data:
npm run localstack:sample-data
- Test the API:
./scripts/test-api.sh
To run all the setup steps together:
npm run dev
npm run localstack:start
- Start LocalStack containersnpm run localstack:stop
- Stop LocalStack containersnpm run localstack:deploy
- Deploy the stack to LocalStacknpm run localstack:sample-data
- Create sample data in DynamoDBnpm run localstack:logs
- View LocalStack logsnpm run dev
- Start the app locally in development mode (runs start, deploy, and sample-data)
The Survey App provides a RESTful API for managing surveys and responses. Authentication is handled through either:
- API Key (for LocalStack development)
- Cognito User Pools (for production)
Method | Endpoint | Description | Auth Required |
---|---|---|---|
POST |
/surveys |
Create a new survey | Yes |
GET |
/surveys |
List all surveys (filtered by ownership/public) | Optional |
GET |
/surveys/{surveyId} |
Get a specific survey | Optional |
POST |
/surveys/{surveyId} |
Update a survey | Yes |
POST |
/surveys/{surveyId}/responses |
Submit a response to a survey | No |
GET |
/surveys/{surveyId}/results |
Get survey results | Yes |
// POST /surveys
// Request
{
"title": "Customer Satisfaction Survey",
"description": "Please tell us about your experience",
"questions": [
{
"id": "q1",
"text": "How would you rate our service?",
"type": "rating",
"required": true
},
{
"id": "q2",
"text": "Any additional comments?",
"type": "text",
"required": false
}
],
"isPublic": true
}
// Response
{
"surveyId": "abc123",
"title": "Customer Satisfaction Survey",
"description": "Please tell us about your experience",
"questions": [...],
"ownerId": "user123",
"createdAt": "2025-03-14T12:00:00.000Z",
"updatedAt": "2025-03-14T12:00:00.000Z",
"isActive": true,
"isPublic": true,
"responseCount": 0
}
// POST /surveys/{surveyId}/responses
// Request
{
"answers": [
{
"questionId": "q1",
"value": "5"
},
{
"questionId": "q2",
"value": "Great service, very satisfied!"
}
],
"metadata": {
"source": "website",
"device": "mobile"
}
}
// Response
{
"message": "Survey response submitted successfully",
"responseId": "resp456"
}
You can test the API using the provided test script:
./scripts/test-api.sh
The application supports two authentication methods:
For local development with LocalStack, API Key authentication is used:
- API keys are created and managed through API Gateway
- The key is included in the
x-api-key
header for all authenticated requests - For testing with LocalStack, the
x-user-id
header can be used to simulate authenticated users
For production deployment:
- User authentication is handled through Amazon Cognito User Pools
- JWT tokens from Cognito are passed in the
Authorization
header - User permissions are based on the Cognito user ID
- API Gateway validates tokens before passing requests to Lambda functions
- Input Validation: All input data is validated before processing
- Permission Controls: Users can only access and modify their own surveys
- Anonymous Responses: Support for anonymous survey responses when enabled
- CORS Configuration: API Gateway is configured with proper CORS headers
⚠️ Note: This codebase contains LocalStack-specific configurations that should be removed or modified for production deployment.
The application includes various LocalStack-specific workarounds to enable local development. These should be properly addressed before deploying to a production AWS environment.
In the Lambda handlers, authentication is bypassed for LocalStack:
// In all Lambda handlers (e.g., createsurvey.ts)
// Check if running in LocalStack
const isLocal = process.env.IS_LOCAL === 'true';
// Get the authenticated user ID
let userId: string;
if (isLocal) {
// In LocalStack, use the x-user-id header
userId = event.headers['x-user-id'] || 'localstack-test-user-id';
console.log('Using mock user ID for LocalStack:', userId);
} else {
// In real AWS, get user ID from Cognito
userId = event.requestContext.authorizer?.claims?.['sub'];
}
Database models use special configuration for LocalStack:
// In models/survey.ts and models/response.ts
// Configure DynamoDB instance
if (isLocal) {
// For LocalStack, use the local endpoint
const ddb = new dynamoose.aws.ddb.DynamoDB({
endpoint: process.env.AWS_ENDPOINT_URL || 'http://localhost:4566',
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: 'test',
secretAccessKey: 'test',
},
});
dynamoose.aws.ddb.set(ddb);
}
The API infrastructure has conditional logic for LocalStack:
// In lib/constructs/api-constructs.ts
// Add environment variables for Lambda functions
if (isLocalStack) {
lambdaFunction.addEnvironment('IS_LOCAL', 'true');
lambdaFunction.addEnvironment(
'AWS_ENDPOINT_URL',
'http://localhost:4566'
);
}
// Different auth mechanism for local vs. production
if (isLocalStack) {
// Use API Key for LocalStack
const apiKey = api.addApiKey('DevApiKey', {
apiKeyName: 'test-api-key-local',
value: 'test-api-key-local-xyz-753',
});
// ...
} else {
// Use Cognito for production
// ...
}
Before deploying to production:
- Remove all
isLocal
conditional checks - Implement proper Cognito authentication
- Remove hardcoded credentials and test API keys
- Remove custom endpoint URLs
- Review all Lambda environment variables
To deploy to AWS:
npx cdk deploy
The following environment variables are used by the Lambda functions:
Variable | Description |
---|---|
SURVEYS_TABLE |
Name of the DynamoDB table for surveys |
RESPONSES_TABLE |
Name of the DynamoDB table for responses |
IS_LOCAL |
Set to 'true' for LocalStack development |
AWS_ENDPOINT_URL |
Custom endpoint URL for LocalStack |
AWS_REGION |
AWS region to use |
The application uses AWS Lambda for serverless API handlers:
Function | Description | Source |
---|---|---|
createSurvey |
Create and update surveys | createsurvey.ts |
getSurveys |
List all surveys and get a specific survey | getsurveys.ts |
submitResponse |
Submit a response to a survey | submitresponse.ts |
getSurveyResults |
Retrieve and analyze survey results | getsurveyresults.ts |
Each Lambda function:
- Uses environment variables for configuration
- Validates input data
- Implements proper error handling
- Checks user permissions where required
- Returns standardized API responses
The application includes comprehensive Jest unit tests for all Lambda handlers:
# Run all unit tests
npm run test:unit
# Run tests for a specific handler
npx jest lambda/handlers/__tests__/createsurvey.test.ts
Unit tests cover:
- Success scenarios
- Input validation
- Error handling
- Permission checks
- Edge cases
Integration tests verify the complete API flow using LocalStack:
# Run all integration tests
npm run test:integration:full
The integration tests are designed to test the entire application flow from end to end:
// test/integration/surveys-api.test.ts
test('Full survey API flow', async () => {
// 1. Create a survey
const surveyToCreate = apiClient.generateTestSurvey();
const createdSurvey = await apiClient.createSurvey(surveyToCreate);
// 2. Get the survey by ID
const retrievedSurvey = await apiClient.getSurvey(createdSurveyId);
// 3. Update the survey
const updatedSurvey = await apiClient.updateSurvey(createdSurveyId, {
title: updatedTitle,
isPublic: true
});
// 4. List all surveys
const surveys = await apiClient.listSurveys();
// 5. Submit a response to the survey
const submittedResponse = await apiClient.submitResponse(
createdSurveyId,
answers,
{ source: 'integration-test' }
);
// 6. Get survey results
const results = await apiClient.getSurveyResults(createdSurveyId);
});
⚠️ Note: Integration tests also rely on LocalStack-specific configurations and may need to be adapted for testing against a real AWS environment.
The tests exercise:
- Creating, reading, and updating surveys
- Submitting responses
- Retrieving survey results
- Permission controls and access restrictions
- Error handling and validation
- Multi-user scenarios
The integration test infrastructure includes utilities for setting up the test environment and managing LocalStack:
// test/integration/utils.ts
export async function setupTestEnvironment(): Promise<string> {
await ensureLocalStackRunning();
await deployCdkAppToLocalStack();
return await getApiEndpoint();
}
survey-app/
├── bin/ # CDK app entry point
├── lib/ # CDK constructs and stack definition
│ ├── constructs/ # Reusable CDK components
│ └── survey-app-stack.ts # Main stack definition
├── lambda/ # Lambda function code
│ ├── handlers/ # API handlers
│ │ ├── __tests__/ # Unit tests for handlers
│ │ ├── createsurvey.ts
│ │ ├── getsurveys.ts
│ │ ├── submitresponse.ts
│ │ └── getsurveyresults.ts
│ ├── layers/ # Lambda layers
│ │ └── common/ # Shared utility functions
│ └── models/ # Data models with Dynamoose
│ ├── index.ts # Model exports
│ ├── survey.ts # Survey model
│ └── response.ts # Response model
├── scripts/ # Development and deployment scripts
└── test/ # Tests
├── integration/ # Integration tests
└── survey-app.test.ts # CDK stack tests