Fetching latest headlines…
Chat with Your Documents: Building a RAG Pipeline with AWS Blocks
NORTH AMERICA
πŸ‡ΊπŸ‡Έ United Statesβ€’July 19, 2026

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

6 views0 likes0 comments
Originally published byDev.to

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.

  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

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

Happy building!

Comments (0)

Sign in to join the discussion

Be the first to comment!