One of the first features users expect from an AI application is deceptively simple:
Upload a document. Ask questions. Get accurate answers.
Whether you're building an internal knowledge portal, customer support assistant, or documentation search, the workflow is remarkably similar.
Users upload files. The application extracts the text, splits it into chunks, generates embeddings, retrieves the most relevant passages, and uses them to ground an LLM's response.
This patternβknown as Retrieval-Augmented Generation (RAG)βhas become the standard way of building AI applications that answer questions using private data.
The individual AWS services are excellent.
The challenge is assembling them into a cohesive developer experience.
A typical implementation requires wiring together S3, document extraction, vector storage, Bedrock, authentication, APIs, background jobs, IAM permissions, and frontend clients. Then you need to figure out how to develop all of it locally without deploying every few minutes.
This is exactly the problem AWS Blocks is designed to solve.
Instead of treating infrastructure, backend code, and frontend clients as separate projects, AWS Blocks treats them as different views of the same application.
Let's build a complete "Chat with your Documents" application and see how that changes the development experience.
What we're building
Our application will support the complete document lifecycle.
Upload Document
β
βΌ
FileBucket Block
β
βΌ
Background Extraction
β
βΌ
KnowledgeBase Block
βββββββββββ΄ββββββββββ
β β
Chunk Documents Create Embeddings
β β
βββββββββββ¬ββββββββββ
β
S3 Vectors
β
βΌ
Semantic Retrieval
β
βΌ
Amazon Bedrock
β
βΌ
Grounded AI Response
The result is a chatbot that only answers questions using the documents your users have uploadedβand can cite exactly where those answers came from.
Getting started
We'll start with a new AWS Blocks application.
npx @aws-blocks/create-blocks-app chat-with-documents
cd chat-with-documents
Check the aws-blocks/ directory where you'll define your Building Blocks, typed APIs, and infrastructure. Throughout this article, we'll focus on extending that project to build a complete Retrieval-Augmented Generation (RAG) application.
Before writing any code, install the AWS Blocks skill from the Agent Toolkit for AWS.
npx skills add https://github.com/aws/agent-toolkit-for-aws --skill aws-blocks
The skill gives your AI coding assistant access to the latest AWS Blocks guidance, validated implementation patterns, common pitfalls, and best practices. Since AWS Blocks is evolving quickly, using the skill helps ensure code generation stays aligned with the current APIs and recommended architecture.
With the project scaffolded and the skill installed, we're ready to build the document ingestion and retrieval pipeline.
Project structure
One of the goals of AWS Blocks is keeping infrastructure, backend APIs, and frontend code together.
Instead of separating CDK projects, Lambda functions, generated SDKs, and frontend applications into different repositories, AWS Blocks encourages a single project where each feature is built as a vertical slice.
A project for this article might look something like this:
chat-with-documents/
βββ aws-blocks/
β βββ scripts/
β βββ client.js
β βββ index.ts
β βββ index.cdk.ts
β βββ index.handler.ts
β βββ package.json
β
βββ src/app/ # Frontend: Nextjs/Nuxt
β βββ components/
β βββ composables/
β βββ pages/
β βββ plugins/
β
βββ knowledge/
β βββ api-reference.md
β
βββ uploads/
β βββ (raw user uploads)
β
βββ AGENTS.md
βββ cdk.json
βββ next.config.ts # or nuxt.config.ts
βββ package.json
βββ tsconfig.json
Each directory has a single responsibility.
| Directory | Purpose |
|---|---|
aws-blocks/ |
Defines your Building Blocks, typed APIs, and the infrastructure required to run them. |
src/app/ |
The Nuxt frontend: pages, components, composables, and plugins. |
knowledge/ |
Documents that are indexed into the Knowledge Base for semantic retrieval. |
uploads/ |
Temporary storage for user-uploaded files before they're extracted and indexed. |
Why RAG is more complicated than it looks
From a user's perspective, chatting with a PDF looks almost magical.
Behind the scenes, quite a lot happens.
- Store uploaded files.
- Extract text from PDFs, Word documents, or images.
- Split large documents into meaningful chunks.
- Generate embeddings.
- Store vectors.
- Retrieve relevant chunks.
- Send only those chunks to the LLM.
- Return an answer with citations.
Every one of these steps introduces infrastructure.
Every one introduces permissions, APIs, deployment concerns, and local development challenges.
Most frameworks focus on making deployment easier.
AWS Blocks focuses on making development easier.
Infrastructure from Code (IFC)
Traditional AWS applications usually look something like this.
CDK
β
CloudFormation
β
AWS Resources
β
SDK
β
Generated Client
β
Frontend
Infrastructure lives in one project.
Application code lives somewhere else.
Generated clients have to stay synchronized.
Local mocks are usually handwritten.
Every deployment creates another opportunity for things to drift apart.
AWS Blocks takes a completely different approach.
Instead of infrastructure generating application code, the application defines the infrastructure.
This pattern is called Infrastructure from Code (IFC).
A single building block contains everything needed for every environment.
KnowledgeBase Block
βββ Infrastructure (CDK)
βββ Runtime implementation
βββ Local mock
βββ Typed client
βββ Shared API
The frontend imports the exact same API that your backend defines.
During local development it automatically talks to local implementations.
During sandbox it talks to real AWS services.
During production it talks to deployed infrastructure.
Your application code never changes.
There are no generated SDKs.
No separate client packages.
No environment-specific implementations.
Just one API.
In Infrastructure as Code (IaC), you first describe infrastructure and then write an application that consumes it.
Infrastructure from Code (IFC) inverts that relationship. You write application code first, and AWS Blocks derives the infrastructure needed to run it.
The magic: one import, multiple implementations
One of the most interesting ideas behind AWS Blocks is that the same import can behave differently depending on where it's running.
Imagine importing a Knowledge Base.
import { KnowledgeBase } from "@aws-blocks/bb-knowledge-base"
That import resolves differently depending on the environment.
| Environment | Implementation |
|---|---|
| Local development | Local mock |
| Sandbox | AWS runtime |
| Production | AWS runtime |
| CDK deployment | Infrastructure definition |
This is powered by JavaScript conditional exports.
Node automatically selects the correct implementation.
Your application never needs if (local) or if (production) logic.
The implementation changes.
Your code does not.
This is one of the biggest reasons the development experience feels dramatically different from traditional cloud applications.
Creating the Knowledge Base
Our application needs somewhere to store embeddings and perform semantic search.
That's exactly what the KnowledgeBase block provides.
import { Scope, ApiNamespace } from "@aws-blocks/blocks"
import { KnowledgeBase } from "@aws-blocks/bb-knowledge-base"
const scope = new Scope("chat-app")
const kb = new KnowledgeBase(scope, "docs", {
source: "./knowledge",
description: "Application documentation",
})
export const api = new ApiNamespace(scope, "api", () => ({
search(query: string) {
return kb.retrieve(query, {
maxResults: 5,
})
},
}))
There's a lot happening in just a few lines of code.
The KnowledgeBase doesn't simply provision AWS resources. It also exposes a runtime API that can be called directly from your application. By defining the API alongside the infrastructure, AWS Blocks eliminates the need to manually create Lambda functions, API Gateway routes, OpenAPI specifications, or generated frontend SDKs.
You don't write separate infrastructure.
You simply declare what your application needs.
This is Infrastructure from Code in practice. The application defines both its behavior and the infrastructure required to run it.
The frontend doesn't need a generated client
Because the backend API is fully typed, the frontend simply imports it and calls it like any other TypeScript function.
const { results } = await api.search(
"How do I rotate API keys?"
)
Notice that the frontend isn't making an HTTP request or calling a generated SDK. It's simply invoking the typed API exposed by the backend.
There isn't a separate SDK to generate or synchronize.
During local development, this call executes against the local mock implementation.
During Sandbox and Production, the exact same code executes against your deployed AWS resources.
The API remains constant. Only the implementation changes.
Uploading documents
The first half of a RAG pipeline isn't AI at all.
It's document processing.
Users upload PDFs, DOCX files, spreadsheets, or images.
Those files need somewhere to live before they can be processed.
AWS Blocks provides a FileBucket for exactly this purpose.
After upload, a background job extracts text and writes it into the knowledge source.
uploads/
βββ contract.pdf
βββ handbook.docx
βββ pricing.pdf
β
βΌ
Extract Text
β
βΌ
knowledge/
βββ contract.md
βββ handbook.md
βββ pricing.md
Notice something important.
The application never manipulates embeddings directly.
It simply produces clean text.
The Knowledge Base handles chunking, indexing, and retrieval automatically.
Chunking matters more than most people realize
One of the biggest factors affecting answer quality isn't the language model.
It's chunking.
Large documents need to be divided into smaller pieces before embeddings are generated.
If chunks are too small, important context gets lost.
If they're too large, retrieval becomes less accurate.
AWS Blocks supports multiple chunking strategies depending on your workload.
| Strategy | Best for |
|---|---|
| Semantic | Documentation, articles, manuals |
| Fixed-size | Predictable chunk sizes |
| Hierarchical | Large technical documents |
| None | Small FAQs or short documents |
For most applications, semantic chunking provides the best balance between retrieval quality and simplicity.
Choosing a strategy is simply another part of configuring the block.
const kb = new KnowledgeBase(scope, "docs", {
source: "./knowledge",
chunking: {
strategy: "semantic",
},
})
Retrieval is where RAG becomes intelligent
When a user asks a question, we don't send every document to the LLM.
Instead, we retrieve only the pieces most relevant to that question.
For example, imagine the user asks:
How do I rotate API keys?
The retrieval engine might return:
- Security Guide β API Key Rotation
- Operations Manual β Secrets Management
- Deployment Guide β Environment Variables
Those chunks become the context for the LLM.
The model never sees unrelated documentation.
That keeps responses faster, cheaper, and significantly more accurate.
Multi-tenant applications
Most SaaS products don't have one knowledge base.
They have hundreds.
Every organization should only retrieve its own documents.
Instead of maintaining separate vector databases, AWS Blocks lets retrieval be filtered using document metadata.
const results = await kb.retrieve(question, {
filter: {
folder: {
equals: workspaceId,
},
},
})
A document stored under workspace-123/ automatically receives the appropriate metadata, allowing retrieval to be scoped to a single tenant without maintaining separate indexes.
The application logic remains exactly the same.
Local development without AWS
This is where AWS Blocks really shines.
Traditional cloud development often looks like this.
Write code
β
Deploy
β
Wait
β
Upload documents
β
Wait
β
Test
β
Repeat
That feedback loop can easily take several minutes.
AWS Blocks replaces it with a local implementation.
During development:
- documents are indexed locally
- the local implementation uses TF-IDF instead of embeddings so indexing is nearly instantaneous and doesn't require an AWS account, while preserving the same retrieval API you'll use in production
- uploads remain on your machine
- APIs stay fully typed
- frontend code behaves exactly like production
No AWS account is required.
No credentials are required.
No deployment is required.
Once you're happy with the experience, you can switch to sandbox.
Your code doesn't change.
Only the implementation does.
Why this feels different: Vertical development instead of service integration
Most cloud applications are built horizontally.
You start by provisioning infrastructure, then expose APIs, then generate SDKs, and finally wire everything into your frontend.
Frontend
β
Generated Client
β
API
β
Lambda
β
S3
β
Bedrock
β
IAM
Each layer is developed independently. A small feature often requires changes across multiple projects, deployments to test the backend, and handoffs between infrastructure and application code.
AWS Blocks encourages a vertical way of building software instead.
Think about a single featureβchat with documents.
Instead of touching five different layers of your stack, you build one vertical slice that owns everything it needs.
Chat with Documents
βββ Upload documents
βββ Extract text
βββ Index knowledge
βββ Retrieve context
βββ Generate answers
βββ Typed API
βββ Local mock
βββ AWS infrastructure
That entire feature lives together as application code.
The infrastructure, runtime implementation, local development experience, and frontend API are simply different representations of the same building blocks.
This changes the way you develop.
Rather than spending days wiring together services before you can test a feature, you build a complete workflow from the UI all the way to the cloud resources behind it.
You can upload a document, ask a question, tweak the retrieval logic, adjust chunking, or refine promptsβall without changing how your frontend communicates with the backend.
Even better, that workflow remains the same across every environment.
Same application code
ββββββββββββ¬βββββββββββ
β β β
Local Sandbox Production
β β β
Local Mock Real AWS Real AWS
During local development, AWS Blocks swaps in local implementations that let you iterate quickly without deploying infrastructure.
When you're ready for integration testing or production, the exact same code automatically runs against real AWS services.
You aren't rewriting your application for each environmentβthe implementation changes underneath you.
This is the core idea behind Infrastructure from Code (IFC).
Infrastructure is no longer something you build first and consume later.
It's simply another capability of the same application code.
Why this approach scales
As applications grow, RAG pipelines usually become more sophisticated.
You might add:
- authentication
- background extraction jobs
- streaming chat responses
- citations
- multiple knowledge bases
- document permissions
- evaluation pipelines
- feedback collection
- Bedrock Guardrails
Traditionally, every feature introduces another AWS service and another integration.
With AWS Blocks, each concern becomes another building block.
The application continues to look like application code rather than infrastructure code.
That separation becomes increasingly valuable as projects become larger.
What we never had to build
It's worth noticing everything that's missing from this application.
- No REST endpoints
- No API Gateway configuration
- No OpenAPI specification
- No generated frontend SDK
- No handwritten local mocks
- No environment-specific code
- No separate infrastructure project
Those capabilities are provided by the Building Blocks themselves.
Your application code stays focused on business logic while AWS Blocks supplies the infrastructure, runtime implementation, local development experience, and typed APIs behind the scenes.
Putting it all together
The finished architecture looks something like this.
Nextjs/Nuxt Frontend
β
Typed API Calls
β
AWS Blocks API
βββββββββββββββββΌββββββββββββββββ
β β β
FileBucket KnowledgeBase Authentication
β β
β Amazon Bedrock
β β
βββββββββββ S3 Vectors ββββββββββ
β
Grounded Responses
Each block owns its infrastructure, runtime implementation, and local development experience.
The frontend simply imports the API.
The development workflow
One of the goals of AWS Blocks is making every environment behave consistently.
| Command | Environment | Purpose |
|---|---|---|
npm run dev |
Local mocks | Fast iteration |
npm run sandbox |
Real AWS | Validate integrations |
npm run deploy |
Production | Deploy application |
Because every environment exposes the same APIs, moving between them is mostly a configuration changeβnot a development task.
Your frontend continues to call the same typed methods throughout the entire lifecycle.
The API stays the same.
Only the implementation underneath it changes.
Final thoughts
Building a RAG application isn't difficult because any single AWS service is complicated.
It's difficult because there are so many moving pieces.
Storage.
Extraction.
Chunking.
Embeddings.
Retrieval.
Authentication.
Background jobs.
Deployment.
Local development.
AWS Blocks approaches this from a different direction.
Rather than asking developers to stitch services together, it packages those integrations into reusable building blocks powered by Infrastructure from Code.
The same objects define your infrastructure, power local development, expose typed APIs, and run in production.
That means less time writing glue code.
Less time waiting for deployments.
More time improving retrieval quality, refining prompts, and building experiences your users actually care about.
As AI-powered applications become increasingly common, that shiftβfrom managing infrastructure to building productsβmay be the biggest advantage of all.
Learn more
- AWS Blocks Documentation β Building Blocks, API reference, patterns
- Agent Toolkit for AWS β The canonical skill for AWS Blocks best practices, security patterns, and common mistakes.
- Bedrock Knowledge Bases β AWS documentation
- Amazon S3 Vectors for cost-optimized vector storage for AI agents, inference, RAG, and semantic search.
- AWS Blocks Getting Started β Project scaffolding and first steps
Happy building!
United States
NORTH AMERICA
Related News
My Local AI Assistant Got Worse When I Remembered Too Much
15h ago
RedHook malware turns on your phone's Wireless Debugging to stream your screen β and it never touches the consent dialog
15h ago
Naming Things Without Pain
15h ago
Running Docker on Proxmox: LXC vs VMs and the Firewall Rules That Actually Matter
14h ago
How to Query Databricks from Salesforce Apex (Without Copying a Billion Rows)
14h ago