Verbo

A fullstack translation app built with Amazon API Gateway, Lambda, DynamoDB, Amazon Cognito, and Amazon Translate.

Tech Stack

NextJSReact Hook FormReact QueryZodAWS AmplifyTailwind CSSShadcnAPI GatewayLambdaDynamoDBAmazon TranslateCognitoS3S3 DeploymentMiddyLambda Powertools

githubGitHubdemoDemo

Verbo Architecture Diagram

What is Verbo?

Verbo Translator is an app that helps users learn new languages by translating text into any of the 72 supported languages. It also provides audio pronunciations and examples of how to use words and phrases in real conversations.

Why did I build Verbo?

I built Verbo to help me learn new languages. I've always been fascinated by how languages shape communication across different cultures. As a bilingual speaker, I know firsthand how challenging it can be to learn a new language. I wanted to create an app that would help my wife and I practice speaking and expand our vocabulary with new words and phrases.

Frontend

The frontend is built with Next.js, Zod, react-hook-form, Tailwind CSS, shadcn/ui, and AWS Amplify.

I chose Next.js primarily for its static site generation capabilities, making it perfect for fast, lightweight applications. Its file-based routing keeps things organized and simple to work with.

Zod handles schema validation, allowing me to define schemas and infer TypeScript types easily. It integrates well with react-hook-form, making it straightforward to manage form state and validation.

For styling, Tailwind CSS provides a utility-first approach, enabling me to style quickly and consistently. I’m also using shadcn/ui to build reusable components with encapsulated styles and type-safe variants, ensuring maintainability.

For state management, I’m using Zustand because it’s lightweight, intuitive, and minimizes boilerplate code. It provides a simple, scalable way to manage global state without the complexities of larger libraries, making it a great fit for this project.

Lastly, I selected Amplify Gen 1 v6 for authentication with Cognito, offering a secure and straightforward way to manage user sign-ups and logins while integrating seamlessly with other AWS services.

Infrastructure

Verbo is actively evolving, and at this point, I have divded the application into the following domains Static Website Hosting, API Layer, Translations, Data Layer, Authentication, CI/CD Pipeline, and Testing.

Static Website Hosting

To host the website I'm using a typical Single Page Application (SPA) setup with an Origin bucket and a CloudFront Distribution in front of it to serve the content.

Here are some of the services I'm using to host the website:

  • Route 53 – Manages DNS and domain routing.
  • CloudFront – Distributes content globally with low latency.
  • CloudFront Functions – Executes lightweight logic at the edge.
  • S3 (Origin & Deployment) – Hosts static files and handles deployments.
  • Amazon ACM TLS Certificate – Secures connections with HTTPS.

API Layer

For the API layer I'm using API Gateway to manage API requests and routes them to backend logic handled by Lambda integrations, and allows you a way to rate limits to ensure users don't abuse the API. A couple other things to note is that I'm planning on using Middy as middleware to handle the Lambda Proxy Integrations for API Gateway, and Lambda Powertools which is a collection of utilities and best practices for working with AWS Lambda.

Services used:

  • API Gateway – Manages API requests and routes them to backend services.
  • Lambda (Integrations) – Handles the backend logic with serverless functions.
  • Middy & Lambda Powertools – Provides middleware and utilities for enhanced Lambda functionality.

Translations

  • Amazon Translate – Powers multilingual translations in real-time.

Data Layer

  • DynamoDB – Manages structured, scalable data storage.

Authentication

  • Cognito – Provides user authentication and management.
  • Cognito Identity Pool – Handles federated identities and access control.

CI/CD Pipeline

  • OIDC Provider – Allows secure integration between AWS and GitHub for deployments.
  • GitHub Actions – Automates build, testing, and deployment workflows.

Testing

  • Jest Cucumber – Supports behavior-driven development (BDD).
  • Integration Testing – Ensures components work together as expected.
  • End-to-End Testing – Verifies the entire system’s behavior from start to finish.

Challenges

The following are some of the challenges I have faced during the development of Verbo:

Enabling CORS on API Gateway

Diagram will go here...

One of the first challenges I faced when building Verbo were CORS issues. CORS (Cross-Origin Resource Sharing) is a security feature built into web browsers that controls how web applications on one domain (origin) can request resources or interact with another domain. It’s designed to prevent malicious websites from making unauthorized requests to your server on behalf of unsuspecting users.

For example, if a frontend hosted at https://frontend.com tries to access data from https://api.backend.com, the browser will block the request unless the API server explicitly allows it through CORS headers.

To enable CORS on API Gateway, you need to add the appropriate headers to the API Gateway response. This is typically done using a CORS configuration in the API Gateway console or through API Gateway's API.

Here's an example of how to enable CORS on API Gateway, but you can also view the source code here.

const createApiGatewayResponse = ({ 
  statusCode, 
  body 
}: CreateHandlerResponseInput): lambda.APIGatewayProxyResult => {
    return {
      statusCode,
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Credentials": true,
        "Access-Control-Allow-Headers": "*",
        "Access-Control-Allow-Methods": "*",
      },
      body,
    };
};

How these headers work?

  • Access-Control-Allow-Origin: This header specifies the origin that is allowed to access the resource. By setting it to *, we are allowing requests from any origin. In a production environment, you should specify the actual domain that is authorized to access the resource.
  • Access-Control-Allow-Credentials: This header is used in conjunction with Access-Control-Allow-Origin to control whether credentials (e.g., cookies, authorization headers) are sent along with the request. By setting it to true, we are allowing the request to include credentials.
  • Access-Control-Allow-Headers: This header specifies the headers that are allowed in the actual request when the response has a Access-Control-Allow-Origin header with the value *.
  • Access-Control-Allow-Methods: This header specifies the methods that are allowed when the response has an Access-Control-Allow-Origin header with the value *.

How CORS Works with API Gateway:

  1. Preflight Requests: If a request uses methods other than GET or POST (like PUT or DELETE) or includes custom headers, the browser sends a preflight request (an OPTIONS request) to verify if the server accepts the cross-origin request.
  2. OPTIONS Method Response: API Gateway needs to be configured to handle these OPTIONS requests and respond with the correct CORS headers.
  3. Response Headers: When the actual API request is made, the server must also include the relevant CORS headers in the response for it to succeed.

Rate Limiting with API Gateway

Diagram will go here...

Rate limiting in API Gateway controls how many requests can hit your API within a certain time, helping prevent the backend from being overwhelmed by too much traffic—whether that’s from legitimate usage or malicious activity like DDoS attacks. It ensures the API stays available and performs well for everyone. AWS applies account-level rate limits by default, but you can also set custom limits using Usage Plans tied to API Keys. Usage Plans let you define how many requests per second (rate limit) and how many extra requests can be handled in short bursts (burst limit) before API Gateway returns a 429 Too Many Requests response.

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
 
export class ApiGatewayRateLimitStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
 
    // Create a Lambda function to handle API requests
    const myLambda = new lambda.Function(this, 'MyLambdaHandler', {
      runtime: lambda.Runtime.NODEJS_18_X,
      code: lambda.Code.fromAsset('lambda'), // Assuming your code is in 'lambda' directory
      handler: 'index.handler',
    });
 
    // Create API Gateway and integrate it with the Lambda function
    const api = new apigateway.RestApi(this, 'MyApi', {
      restApiName: 'MyPublicAPI',
      description: 'Public API with global rate limiting.',
      deployOptions: {
        stageName: 'prod',
        throttlingRateLimit: 1, // Max 1 request per second
        throttlingBurstLimit: 2, // Max 2 requests in a short burst
      },
    });
 
    // Integrate Lambda with the API Gateway resource
    const lambdaIntegration = new apigateway.LambdaIntegration(myLambda);
    const resource = api.root.addResource('hello');
    resource.addMethod('GET', lambdaIntegration);
 
    // Set up a Usage Plan with a global quota for the API stage (no API key required)
    const usagePlan = new apigateway.UsagePlan(this, 'GlobalUsagePlan', {
      name: 'GlobalUsagePlan',
      throttle: {
        rateLimit: 1, // Limit to 1 request per second
        burstLimit: 2, // Allow short bursts of up to 2 requests
      },
      quota: {
        limit: 50, // Max 50 requests per day
        period: apigateway.Period.DAY, // Quota resets daily
      },
    });
 
    // Associate the usage plan with the API stage (no API key required)
    usagePlan.addApiStage({
      stage: api.deploymentStage,
    });
  }
}

Configuring private access to the API Gateway

Diagram will go here...

Caching Translation Responses (Future)

Diagram will go here...

In Progress ...

This application is still under development, but it is already in a working state. I'm currently working on improving the code quality and adding new features.