Chat with Your Documents: Building a RAG Pipeline with AWS Blocks

작성자

카테고리:

← 피드로
DEV Community · Saddam Azad · 2026-07-20 개발(SW)

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 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.

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.

  1. Store uploaded files.
  2. Extract text from PDFs, Word documents, or images.
  3. Split large documents into meaningful chunks.
  4. Generate embeddings.
  5. Store vectors.
  6. Retrieve relevant chunks.
  7. Send only those chunks to the LLM.
  8. 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 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,
    })
  },
}))

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 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",
  },
})

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 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

Happy building!

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다