avatar Post

Building Serverless Apps with AI: The AWS Starter I Use for Every Project

Building Serverless Apps with AI: The AWS Starter I Use for Every Project

Over the past year, I’ve built several full-stack serverless applications on AWS. And something changed: with AI coding agents, writing the application itself is no longer the slowest part of the process.

The time-consuming part is increasingly everything around the code: how to structure the repository, which patterns to follow, how authentication should work, how to organize infrastructure, how to share types between frontend and backend, and how to deploy everything consistently.

Before I had this sorted out, every new project meant making the same architectural decisions and wiring the same pieces together again. AI made that problem more visible, not less. If you give an agent an inconsistent codebase, it will happily reproduce that inconsistency at scale.

So I stopped starting from scratch.

I extracted the common foundation from my applications into a reusable GitHub template and published it as serverless-monorepo-aws-starter.

The problem changed

Building a full-stack serverless application still means integrating several concerns:

  • Authentication with Amazon Cognito, MFA, roles, and login flows
  • Infrastructure as Code for Amazon DynamoDB, Amazon API Gateway, AWS Lambda, Amazon CloudFront, and Amazon S3
  • CI/CD for linting, testing, building, and deployment
  • A frontend with routing, authentication state, and PWA support
  • Shared contracts between frontend and backend
  • Conventions for linting, formatting, commit hooks, project structure, and deployment

None of these is difficult in isolation. The friction comes from making all of them work together consistently.

AI can generate each piece very quickly. But without a clear foundation, the result can become a collection of locally reasonable decisions that do not form a coherent system.

That is exactly what I wanted to avoid.

The idea: standardize the boring parts

The skeleton is commodity. It should be boring, stable, and rarely touched. The value lives in the product you build on top.

I want new applications to start with the same conventions, the same deployment model, the same authentication flow, and the same overall structure.

This helps humans: moving between projects and onboarding contributors becomes easier.

But it also helps AI agents. Instead of asking an agent to invent an architecture every time, I can ask it to extend an architecture that already exists.

That difference is much more important than it sounds.

Architecture at a glance

Architecture of the serverless monorepo AWS starter The baseline architecture: a React SPA behind CloudFront, Cognito authentication, an API Gateway HTTP API, ARM64 Lambda functions, DynamoDB, and deployments driven by GitHub Actions and AWS CDK.

The application is intentionally simple at the infrastructure level:

  1. The React/Vite frontend is stored in a private S3 bucket and served through CloudFront using Origin Access Control (OAC).
  2. Users authenticate with Amazon Cognito and complete TOTP MFA.
  3. The frontend calls an API Gateway HTTP API using the user’s JWT.
  4. API routes invoke domain-oriented AWS Lambda handlers.
  5. Lambda functions read and write application data in DynamoDB.
  6. AWS CDK defines the infrastructure, while GitHub Actions uses OIDC to deploy without storing long-lived AWS credentials in GitHub.

What you get

The starter gives you a tested, working, opinionated baseline that can be deployed to your own AWS account:

LayerWhat’s included
AuthCognito User Pool with mandatory TOTP MFA, roles (ADMIN/USER), and a complete login flow including password change, reset, and TOTP setup
BackendARM64 Lambda functions in TypeScript ESM, one handler per domain, zod validation, and JWT verification
Infra4 AWS CDK stacks: storage (DynamoDB), auth (Cognito), API (HTTP API + Lambdas), and frontend (private S3 + CloudFront with OAC and security headers)
FrontendReact 18 + Vite + Tailwind, PWA support with controlled update prompt, iOS safe-area support, and Amplify authentication
Shared@app/shared workspace with types, constants, and zod schemas shared between frontend and backend
CI/CDGitHub Actions with OIDC, running lint → test → build → deploy; non-main branches deploy to test and main deploys to prod
DeploysHash-based incremental deployment that skips components whose version-controlled inputs have not changed
ToolingMakefile, husky, ESLint 9, Prettier, tests, and unified commands

It is not intended to be the final architecture for every possible product. It is the baseline I want available before writing the first line of product-specific code.

Why this works particularly well with AI agents

A predictable codebase as a foundation an AI agent can build on A predictable, well-structured codebase acts as guardrails: the more consistent the foundation, the more reliably an AI agent can extend it.

One thing I’ve learned from building applications with coding agents is that the more predictable the codebase is, the more useful the agent becomes.

The starter is not only boilerplate for developers. It also acts as a set of architectural guardrails for AI.

There is a big difference between asking:

Build me a serverless application on AWS.

and asking:

Add this entity following the existing Item pattern. Reuse the current shared schemas, Lambda handler conventions, CDK stacks, API client, and authentication model.

The second prompt gives the agent constraints and examples instead of a blank canvas.

The repository includes a detailed starter prompt that can be used with Kiro or another coding agent. The intended workflow is:

  1. Define the business entity and its relationships.
  2. Let the agent propose the DynamoDB model and API routes first.
  3. Review that design before code generation.
  4. Implement one complete vertical slice: shared → backend → infra → frontend.
  5. Keep npm run build and npm run validate green.
  6. Deploy and verify the feature before moving to the next entity.

This is how I prefer to work now: small, scoped tasks on top of a predictable foundation.

The reference domain: Items + Shares

The starter includes a complete Item example with:

  • Per-user ownership: users only see the items they own or that have been shared with them
  • Read/write sharing between users
  • Full CRUD with zod validation
  • User-management endpoints
  • Authorization patterns that can be reused by other entities

This is deliberately more useful than a hello-world Lambda.

The goal is not to keep Item forever. The goal is to give both the developer and the AI agent a realistic vertical slice to imitate when building the actual domain.

Getting started: two placeholders

There are only two project-specific tokens to replace across the repository:

1
2
   # e.g. my-awesome-app
     # e.g. eu-south-2

Then validate and deploy the starter:

1
2
3
4
5
6
7
8
9
10
npm install
npm run build

# Required once per AWS account/region
cdk bootstrap aws://<account>/<region>

make deploy ENV=test
make create-admin EMAIL=you@email.com PASSWORD='Temp.123!' ENV=test
make dev-env
make dev

At that point you have a deployed application with authentication, TOTP MFA, API endpoints, CRUD, and a frontend — without spending days wiring the foundation together first.

Structure at a glance

1
2
3
4
5
6
7
8
9
.
├── shared/             # @app/shared — types, constants, zod schemas
├── frontend/           # React SPA (Vite + Tailwind + PWA + Amplify)
├── backend/            # Lambda handlers per domain
├── infra/cdk/          # CDK: storage + auth + api + frontend stacks
├── scripts/            # deploy, create-user, set-password, dev-frontend
├── .github/workflows/  # pipeline.yml (lint → test → build → deploy)
├── Makefile            # unified command interface
└── tsconfig.base.json  # shared TypeScript config

Key design decisions

A few choices are worth highlighting.

  • ARM64 Lambda functions: the starter uses AWS Graviton-based Lambda execution. For many workloads ARM64 can provide attractive price/performance, while still keeping the application model exactly the same.
  • HTTP API instead of REST API: the application does not need the broader feature set of API Gateway REST APIs, so HTTP API keeps the API layer simpler and generally cheaper.
  • Private S3 + CloudFront with OAC: the frontend bucket is never public. CloudFront accesses it through Origin Access Control rather than the legacy Origin Access Identity model.
  • OIDC for CI/CD: GitHub does not need long-lived AWS access keys. The workflow obtains short-lived AWS credentials by assuming an IAM role through OpenID Connect.
  • Incremental deployments: scripts/deploy.sh fingerprints version-controlled inputs and skips components that have not changed.
  • Monorepo with npm workspaces: shared types and validation schemas are a local workspace package instead of a separately published dependency.
  • Two explicit environments: the baseline uses test and prod. Non-main branches deploy to test; main deploys to prod.

The current repository uses Node.js 22 for Lambda. Runtime versions are intentionally an implementation detail rather than part of the architectural contract, so they can evolve independently of the overall pattern.

What I deliberately left out

A starter becomes less useful when it tries to solve every possible architecture.

This repository deliberately does not attempt to include everything:

  • Kubernetes or container orchestration
  • Relational databases by default
  • Microservice decomposition
  • Complex event-driven workflows
  • Multi-region architecture
  • Enterprise multi-account governance
  • Every possible observability, compliance, or security control

Those are all valid requirements when the product needs them. They are simply not complexity I want to pay for by default in every new application.

The principle is: start with the simplest architecture that satisfies the problem, then add complexity when there is a concrete reason to do so.

Who is this for

This starter is a good fit for:

  • Developers building side projects, prototypes, SaaS products, or internal tools on AWS
  • Teams that want a standardized starting point for new serverless applications
  • Developers using AI coding agents who want the agent to extend established patterns instead of inventing new ones
  • Projects where DynamoDB, Lambda, API Gateway, Cognito, and a React SPA are a reasonable architectural fit

When I would not use it

I would not start from this template if the application fundamentally requires a different architecture, for example:

  • Long-running or compute-heavy workloads better suited to containers
  • A relational domain where SQL transactions and relationships are central to the model
  • A mature enterprise platform that already imposes different CI/CD, identity, networking, or governance standards
  • A system whose primary architecture is asynchronous/event-driven rather than request/response

The point of a starter is not to eliminate architectural thinking. It is to eliminate repetitive architectural setup when the same baseline is already a good fit.

Try it

The repository is public and MIT-licensed:

👉 github.com/alazaroc/serverless-monorepo-aws-starter

AI has dramatically reduced the cost of writing code. That does not make architecture less important — it makes good defaults more valuable.

My goal with this starter is simple: make the infrastructure and project structure boring enough that both humans and AI agents can focus on the part that is actually different every time: the product.

If you build something with it, have suggestions, or find a pattern that should become part of the baseline, let me know in the comments.

This post is licensed under CC BY 4.0 by the author.