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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
Each directory has a single responsibility.
Directory Purposeaws-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.
Schema and document storage
Every document that flows through the pipeline needs metadata—file name, upload status, extraction results, workspace ownership.
AWS Blocks uses Zod schemas to define typed tables that work locally and in DynamoDB.
import { z } from 'zod'
import { DistributedTable } from '@aws-blocks/blocks'
const documentMetaSchema = z.object({
documentId: z.string(),
workspaceSlug: z.string(),
key: z.string(),
status: z.enum(['UPLOADED', 'PROCESSING', 'PROCESSED', 'INDEXED', 'FAILED']),
fileName: z.string(),
fileType: z.string(),
fileSize: z.number(),
uploadedAt: z.string(),
pageCount: z.number().optional(),
confidence: z.number().optional(),
})
Enter fullscreen mode Exit fullscreen mode
The schema defines both the shape of your data and the constraints on it.
A DistributedTable pairs that schema with a key structure and optional indexes.
const documents = new DistributedTable(root, 'documents', {
schema: documentMetaSchema,
key: { partitionKey: 'documentId' },
indexes: {
byWorkspaceSlug: { partitionKey: 'workspaceSlug', sortKey: 'uploadedAt' },
},
})
Enter fullscreen mode Exit fullscreen mode
The byWorkspaceSlug index lets you query all documents for a workspace, sorted by upload time—exactly what the document list view needs.
You can define as many tables as your application requires. Extraction metadata, for example, lives in its own table with a different shape.
const extracted = new DistributedTable(root, 'extracted', {
schema: z.object({
documentId: z.string(),
pageCount: z.number(),
confidence: z.number(),
rawTextPreview: z.string(),
tablesCount: z.number().optional(),
keyValuesCount: z.number().optional(),
formsCount: z.number().optional(),
signaturesCount: z.number().optional(),
extractedAt: z.string(),
}),
key: { partitionKey: 'documentId' },
})
Enter fullscreen mode Exit fullscreen mode
Each table is a building block—locally it uses an in-memory store, in production it provisions DynamoDB. Your application code calls documents.put() or documents.query() the same way in both environments.
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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"
Enter fullscreen mode Exit fullscreen mode
That import resolves differently depending on the environment.
Environment Implementation Local development Local mock Sandbox AWS runtime Production AWS runtime CDK deployment Infrastructure definitionThis 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,
})
},
}))
Enter fullscreen mode Exit fullscreen mode
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?"
)
Enter fullscreen mode Exit fullscreen mode
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.
const bucket = new FileBucket(root, 'storage', {
corsRules: [
{
allowedOrigins: ['*'],
allowedMethods: ['GET', 'PUT', 'POST', 'HEAD'],
allowedHeaders: ['*'],
exposedHeaders: ['ETag'],
maxAge: 3600,
},
],
})
Enter fullscreen mode Exit fullscreen mode
The FileBucket block provisions an S3 bucket and exposes a typed API for presigned URLs, uploads, and deletes. During local development it uses a local file system implementation—no AWS account required.
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
Enter fullscreen mode Exit fullscreen mode
The upload flow uses a two-step pattern: the frontend requests a presigned URL, uploads directly to S3, then confirms the upload so the backend can track it.
async initiateUpload(workspaceSlug: string, fileName: string, fileType: string, fileSize: number) {
const documentId = crypto.randomUUID()
const key = `uploads/${workspaceSlug}/${documentId}/${fileName}`
const presignedUrl = await bucket.putUrl(key, { contentType: fileType })
return { documentId, key, presignedUrl }
},
async confirmDocument(workspaceSlug: string, documentId: string, key: string, fileType: string, fileSize: number) {
await documents.put({
documentId,
workspaceSlug,
key,
status: 'UPLOADED',
fileName: key.split('/').pop() ?? 'unknown',
fileType,
fileSize,
uploadedAt: new Date().toISOString(),
})
return { documentId }
},
Enter fullscreen mode Exit fullscreen mode
The frontend calls initiateUpload to get a URL, uploads the file directly using fetch, then calls confirmDocument to persist the metadata. The backend never touches the file contents—it just coordinates the flow.
Once a document is confirmed, an S3 event notification triggers automatic extraction.
const textractProcessorFn = new NodejsFunction(stack, 'TextractProcessorFunction', {
entry: join(__dirname, '..', 'lambdas', 'textract-pipeline', 'index.ts'),
handler: 'handler',
runtime: Runtime.NODEJS_22_X,
timeout: Duration.minutes(15),
memorySize: 1024,
})
rawBucketResource.addEventNotification(
s3.EventType.OBJECT_CREATED,
new LambdaDestination(textractProcessorFn)
)
Enter fullscreen mode Exit fullscreen mode
The extraction Lambda receives the S3 event, runs Amazon Textract to extract text and structure, then writes the results to the processed bucket. The document status transitions from UPLOADED to PROCESSING to PROCESSED as each stage completes.
Notice something important.
The application never manipulates embeddings directly.
It simply produces clean text.
The Knowledge Base handles chunking, indexing, and retrieval automatically.
Wiring the CDK
The blocks define what your application needs—the CDK connects those needs to real AWS resources.
A separate index.cdk.ts file handles the infrastructure wiring: environment variables, IAM permissions, and event triggers.
export default function (stack: BlocksStack) {
stack.handler.addEnvironment('METADATA_TABLE_NAME', documentsTable.tableName)
stack.handler.addEnvironment('OUTPUT_BUCKET', processedBucketResource.bucketName)
}
Enter fullscreen mode Exit fullscreen mode
Each block automatically provisions its core resources. The CDK file adds the glue—passing resource names between blocks and granting the permissions they need.
IAM policies follow the same pattern. The chat endpoint needs Bedrock access, so you add it to the handler’s role.
stack.handler.addToRolePolicy(new iam.PolicyStatement({
actions: [
'bedrock:InvokeModel',
'bedrock:InvokeModelWithResponseStream',
'bedrock:Retrieve',
],
resources: ['*'],
}))
stack.handler.addToRolePolicy(new iam.PolicyStatement({
actions: [
'bedrock:ListDataSources',
'bedrock:GetDataSource',
'bedrock:StartIngestionJob',
],
resources: ['*'],
}))
Enter fullscreen mode Exit fullscreen mode
The extraction Lambda needs its own permissions—Textract for document analysis, S3 for reading uploads and writing processed output.
textractProcessorFn.addToRolePolicy(new iam.PolicyStatement({
actions: [
'textract:StartDocumentAnalysis',
'textract:GetDocumentAnalysis',
],
resources: ['*'],
}))
rawBucketResource.grantRead(textractProcessorFn)
processedBucketResource.grantReadWrite(textractProcessorFn)
documentsTable.grantReadWriteData(textractProcessorFn)
Enter fullscreen mode Exit fullscreen mode
The CDK file is where blocks meet the real cloud. Your application code stays focused on business logic—the CDK handles resource naming, permissions, and event routing.
The extraction layer
Raw uploads need to be processed before they can be indexed.
AWS Blocks provides AsyncJob for background work that might take minutes—like running Textract on a 50-page PDF.
const processJob = new AsyncJob(root, 'processor', {
handler: async (payload: Record<string, unknown>) => {
console.log('[processor] received payload:', payload)
},
})
Enter fullscreen mode Exit fullscreen mode
An AsyncJob block provisions a Lambda function backed by an SQS queue with a dead-letter queue. Your handler processes one message at a time, and AWS Blocks manages retries and failure handling automatically.
The Textract pipeline is wired as an S3 event handler—when a file lands in the raw bucket, it’s automatically picked up and processed.
const textractProcessorFn = new NodejsFunction(stack, 'TextractProcessorFunction', {
entry: join(__dirname, '..', 'lambdas', 'textract-pipeline', 'index.ts'),
handler: 'handler',
runtime: Runtime.NODEJS_22_X,
timeout: Duration.minutes(15),
memorySize: 1024,
})
rawBucketResource.addEventNotification(
s3.EventType.OBJECT_CREATED,
new LambdaDestination(textractProcessorFn)
)
Enter fullscreen mode Exit fullscreen mode
The pipeline extracts text, tables, forms, and signatures from the document, then writes the results to the processed bucket as structured JSON. Each chunk gets its own file alongside a metadata file that the Knowledge Base will use during indexing.
The application can check extraction status through a simple endpoint.
async processDocument(_workspaceSlug: string, documentId: string) {
const doc = await documents.get({ documentId })
if (!doc) throw new Error('Document not found')
return { status: doc.status, pageCount: doc.pageCount, confidence: doc.confidence }
},
Enter fullscreen mode Exit fullscreen mode
Once extraction completes, the document status transitions to PROCESSED and the chunks are ready to be synced to the Knowledge Base.
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 documentsFor 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",
},
})
Enter fullscreen mode Exit fullscreen mode
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.
The core retrieval call is straightforward.
const results = await kb.retrieve(query, {
maxResults: 5,
filter: { documentId: { equals: docId } },
})
Enter fullscreen mode Exit fullscreen mode
You pass a query and optionally filter by metadata—workspace, document ID, or any attribute your schema defines. The Knowledge Base returns scored passages with their source text and metadata.
When users upload multiple documents, you need balanced results across all of them. A simple retrieve might return five chunks from the same document, missing relevant passages from others.
The solution is per-document quota allocation—divide your total results evenly across documents, then merge and re-rank.
async retrievePassages(_workspaceSlug: string, query: string, documentIds: string[], topK = 10) {
if (documentIds.length === 0) return []
const resultsPerDoc = Math.ceil(topK / documentIds.length)
const allResults: Array<{ text: string; score: number; source: string; metadata: Record<string, string> }> = []
for (const docId of documentIds) {
const results = await kb.retrieve(query, {
maxResults: resultsPerDoc,
filter: { documentId: { equals: docId } },
})
allResults.push(...results)
}
allResults.sort((a, b) => b.score - a.score)
return allResults.slice(0, topK)
}
Enter fullscreen mode Exit fullscreen mode
This pattern ensures every uploaded document gets a fair chance to contribute relevant passages, and the final ranking is based on relevance scores across the entire set.
The Chat Endpoint
This is where everything comes together—retrieval, context assembly, and language model generation.
The chat endpoint takes a user’s question and conversation history, retrieves relevant passages, and sends them to Bedrock for a grounded response.
Here’s the simplest version: extract the user’s query, retrieve passages, and call the model.
async chat(_workspaceSlug: string, documentIds: string[], messages: Array<{ role: string; content: string }>) {
const lastUser = [...messages].reverse().find(m => m.role === 'user')
const query = lastUser?.content ?? ''
const passages: Array<{ text: string; score: number; metadata: Record<string, unknown> }> = []
for (const docId of documentIds) {
const results = await kb.retrieve(query, {
maxResults: 3,
filter: { documentId: { equals: docId } },
})
passages.push(...results)
}
passages.sort((a, b) => b.score - a.score)
const bedrockClient = new BedrockRuntimeClient({})
const response = await bedrockClient.send(new ConverseCommand({
modelId: 'amazon.nova-lite-v1:0',
messages: [{ role: 'user', content: [{ text: query }] }],
inferenceConfig: { maxTokens: 1024 },
}))
return response.output?.message?.content?.map(c => c.text).join('')
}
Enter fullscreen mode Exit fullscreen mode
That works, but it doesn’t ground the model’s response in the retrieved passages. The model will answer from its training data, not from the documents.
The key is building a context block from the passages and embedding it in the user message alongside the original question.
const contextBlock = passages.length > 0
? passages
.map((p, i) => `[Passage ${i + 1}]${p.metadata?.pageNumber ? ` (page ${p.metadata.pageNumber})` : ''} ${p.text}`)
.join('\n\n')
: 'No passages retrieved for this document.'
const finalUserContent = query
? `DOCUMENT PASSAGES:\n${contextBlock}\n\nMY QUESTION:\n${query}`
: contextBlock
const bedrockMessages = [
...history,
{ role: 'user', content: [{ text: finalUserContent }] },
]
Enter fullscreen mode Exit fullscreen mode
A system prompt tells the model how to use those passages—answer only from the provided context, cite passages by page number, and never fabricate information.
const systemPrompt = [
'You are a document assistant helping a user understand their documents.',
'Answer ONLY based on the document passages provided in the user message.',
'For every factual claim cite the relevant passage using [page N] notation.',
'If the answer cannot be found in the provided passages, say so explicitly — do not fabricate.',
'Be concise. Use clear, professional language.',
].join('\n')
Enter fullscreen mode Exit fullscreen mode
Finally, the response should include citations—the passages that contributed to the answer, scored by relevance.
const citations = passages
.filter(p => (p.score ?? 0) > 0.3)
.slice(0, 3)
.map(p => ({
passage: (p.text ?? '').substring(0, 200),
documentId: (p.metadata?.documentId as string) ?? documentIds[0],
pageNumber: p.metadata?.pageNumber as number | undefined,
score: p.score ?? 0,
}))
return { answer, citations }
Enter fullscreen mode Exit fullscreen mode
The complete endpoint handles retrieval, context assembly, prompt engineering, model invocation, and citation extraction in a single typed method—no separate Lambda, no API Gateway, no generated SDK.
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,
},
},
})
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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 Purposenpm 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!
답글 남기기