Developers are still wiring massive build pipelines just to call an LLM. With Node 22 you can drop the compile step and talk to GPT‑6 Astra in a single TypeScript file. The result is a lean, fast‑to‑prototype chatbot you can run locally or deploy as a Lambda‑free service.
Why GPT‑6 Astra on Bedrock Matters
Large language models (LLMs) are the “brain” behind chat‑style applications. GPT‑6 Astra is Amazon’s newest, high‑throughput model that excels at reasoning and code generation. Amazon Bedrock is the managed service that hosts these models, handling scaling, security, and hardware for you.
In plain English: Bedrock lets you treat a powerful AI model like an ordinary web API—no GPU farms to maintain.
Why choose this combo over a self‑hosted model?
| Reason | What it means for you |
|---|---|
| Zero infrastructure | No need to spin up EC2 instances or containers. |
| Pay‑as‑you‑go | You only pay for the tokens you actually send and receive. |
| Automatic updates | When Amazon releases a newer version of Astra, you get it instantly. |
| Built‑in security | IAM roles control who can invoke the model, and traffic stays inside AWS. |
The only thing that can feel “heavy” is the AWS SDK that many tutorials import. You’ll see that with Node 22’s native fetch and a tiny helper for request signing, the code shrinks dramatically.
Known Bedrock Gotchas (keep these in mind)
- Token limits per minute – Bedrock throttles by the total number of tokens processed across all requests, not by request count.
- Streaming requires manual SSE parsing – The service streams newline‑delimited JSON (NDJSON), not a tidy JSON array.
- Knowledge Base sync delay – New documents take a few minutes to become searchable.
- Model‑specific fine‑tuning support – Not every model can be fine‑tuned; Astra currently runs only as‑is.
- Cross‑region latency – If your compute lives far from the Bedrock region, round‑trip time can increase under load.
Node 22 Makes TypeScript Run‑time‑Ready
Node 22 introduced two features that make a TypeScript‑only workflow possible:
-
Native
fetch– You can call HTTP endpoints without pulling innode-fetchor other polyfills. -
Experimental
--experimental-strip-typesflag – When you run a.tsfile with this flag, Node strips out all TypeScript type annotations on the fly, turning the file into plain JavaScript before execution.
Key takeaway: You write TypeScript for readability and safety, but you never have to run a separate
tsccompilation step.
Running a TypeScript file with the flag
node --experimental-strip-types chat.ts
-
chat.tsis your source file, written with type annotations. - Node reads the file, removes the type syntax, and executes the resulting JavaScript immediately.
Tip: Keep the file extension
.tsso your editor still offers type checking and autocomplete.
Minimal Node 22 boilerplate
// chat.ts – run with `node --experimental-strip-types chat.ts`
// Import only what we need – no heavy SDKs here.
import { fromIni } from "@aws-sdk/credential-provider-ini";
import { SignatureV4 } from "@aws-sdk/signature-v4";
import { Sha256 } from "@aws-crypto/sha256-js";
// The region where your Bedrock model lives.
const REGION = "us-east-1";
The above snippet shows that we can still use a tiny part of the AWS SDK (just the credential provider and signer) without pulling in the full Bedrock client.
Calling Bedrock with Native fetch
The Bedrock endpoint for invoking a model looks like:
https://bedrock-runtime.<region>.amazonaws.com/model/gpt-6-astra/invoke
To call it we need three things:
- AWS credentials – usually obtained from the default credential chain (environment variables, shared config file, etc.).
- SigV4 signing – AWS expects each request to be signed with the Signature Version 4 algorithm.
-
A correctly shaped JSON body – the model expects a
messagesarray similar to the OpenAI chat format.
Signing a fetch request
Below is a complete, commented function that prepares a signed request using the SDK’s SignatureV4 class:
/**
* Build a signed fetch request for Bedrock.
*
* @param body The JSON payload we want to POST.
* @returns A ready‑to‑use Request object with Authorization headers.
*/
async function buildSignedRequest(body: Record<string, unknown>): Promise<Request> {
// 1️⃣ Load credentials – here we use the shared INI file (~/.aws/credentials)
const credentials = await fromIni({ profile: "default" })();
// 2️⃣ Create a SigV4 signer for the Bedrock service
const signer = new SignatureV4({
credentials,
service: "bedrock-runtime",
region: REGION,
sha256: Sha256,
});
// 3️⃣ Assemble the raw HTTP request data (method, URL, headers, body)
const url = `https://bedrock-runtime.${REGION}.amazonaws.com/model/gpt-6-astra/invoke`;
const request = new Request(url, {
method: "POST",
headers: {
"content-type": "application/json",
// Bedrock requires a short user‑agent; keep it simple.
"User-Agent": "bedrock-chatbot-demo/1.0",
},
body: JSON.stringify(body),
});
// 4️⃣ Let the signer add the Authorization header and related X‑Amz‑* headers.
const signed = await signer.sign(request, { signingDate: new Date() });
return signed;
}
In plain English: Think of the signer as a notary that stamps your HTTP letter with a secret code, proving it really came from you.
A tiny example payload
const payload = {
// “messages” follows the chat format: role (system, user, assistant) + content.
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "What’s the capital of France?" },
],
// Optional: max tokens, temperature, etc.
max_tokens: 512,
temperature: 0.7,
};
Now we can send the request:
async function invokeBedrock(payload: Record<string, unknown>) {
const signedRequest = await buildSignedRequest(payload);
// Native fetch returns a Response object we can stream from.
return fetch(signedRequest);
}
Streaming Responses and Handling Chunks
Bedrock streams its answer as newline‑delimited JSON (NDJSON). Each line is a separate JSON object, like:
{"type":"message","content":"Paris"}
{"type":"end"}
If you try to parse the whole response as a single JSON string you’ll get a syntax error. The fix is to read the response body as a stream, split on the newline character (\n), discard empty lines, and parse each chunk individually.
Analogy: a faucet versus a bucket
Imagine you’re filling a bucket (a full JSON array) by turning on a faucet that drips one drop at a time (individual JSON lines). Trying to treat the drips as if you already have a full bucket will fail. You need to collect each drop, line them up, and then understand the whole picture.
Reading the stream
/**
* Consume a streaming NDJSON response and call `onChunk` for each parsed object.
*
* @param response The fetch Response whose body is a readable stream.
* @param onChunk Callback invoked with each JSON object.
*/
async function handleNdjsonStream(
response: Response,
onChunk: (obj: any) => void
): Promise<void> {
// The body is a WHATWG ReadableStream.
const reader = response.body?.getReader();
if (!reader) throw new Error("Response body is not a stream.");
const decoder = new TextDecoder("utf-8");
let buffer = "";
// Loop until the stream signals “done”.
while (true) {
const { value, done } = await reader.read();
if (done) break; // No more bytes.
// Convert Uint8Array → string and append to our buffer.
buffer += decoder.decode(value, { stream: true });
// Split on newline – keep any partial line for the next chunk.
const lines = buffer.split("\n");
// The last element may be incomplete, so keep it in `buffer`.
buffer = lines.pop()!;
for (const line of lines) {
if (!line.trim()) continue; // Skip empty lines.
try {
const obj = JSON.parse(line);
onChunk(obj);
} catch (e) {
console.warn("Failed to parse line:", line);
}
}
}
// If anything remains after the loop, try to parse it.
if (buffer.trim()) {
try {
onChunk(JSON.parse(buffer));
} catch {
// Silently ignore – usually nothing left.
}
}
}
Tip: The
TextDecoderwith{ stream: true }ensures characters that span two chunks (e.g., multi‑byte UTF‑8) are decoded correctly.
Detecting the final answer
Astra marks the end of a response with an object whose type is "end" (or sometimes "metadata"). In a simple chatbot we can just accumulate content fields until we see "end".
function collectAnswer() {
let answer = "";
return (chunk: any) => {
if (chunk.type === "message") {
answer += chunk.content;
} else if (chunk.type === "end") {
console.log("\nAssistant:", answer.trim());
}
};
}
Putting It All Together: A Minimal Chatbot
Below is a self‑contained TypeScript file that:
- Reads a user prompt from the command line.
- Sends the conversation to Bedrock using a signed
fetch. - Streams the NDJSON response, builds the assistant’s reply, and prints it.
Save the file as chat.ts and run with node --experimental-strip-types chat.ts.
// chat.ts – run with: node --experimental-strip-types chat.ts
import { fromIni } from "@aws-sdk/credential-provider-ini";
import { SignatureV4 } from "@aws-sdk/signature-v4";
import { Sha256 } from "@aws-crypto/sha256-js";
// ---------- Configuration ----------
const REGION = "us-east-1"; // Change if your model lives elsewhere.
// ---------- Helper: Build a signed request ----------
async function buildSignedRequest(body: Record<string, unknown>): Promise<Request> {
const credentials = await fromIni({ profile: "default" })();
const signer = new SignatureV4({
credentials,
service: "bedrock-runtime",
region: REGION,
sha256: Sha256,
});
const url = `https://bedrock-runtime.${REGION}.amazonaws.com/model/gpt-6-astra/invoke`;
const request = new Request(url, {
method: "POST",
headers: {
"content-type": "application/json",
"User-Agent": "bedrock-chatbot-demo/1.0",
},
body: JSON.stringify(body),
});
return signer.sign(request, { signingDate: new Date() });
}
// ---------- Helper: Stream NDJSON ----------
async function handleNdjsonStream(
response: Response,
onChunk: (obj: any) => void
): Promise<void> {
const reader = response.body?.getReader();
if (!reader) throw new Error("No streaming body.");
const decoder = new TextDecoder("utf-8");
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop()!; // Keep incomplete line.
for (const line of lines) {
if (!line.trim()) continue;
try {
onChunk(JSON.parse(line));
} catch {
console.warn("Bad line:", line);
}
}
}
if (buffer.trim()) {
try {
onChunk(JSON.parse(buffer));
} catch {
// ignore final stray data
}
}
}
// ---------- Main chatbot logic ----------
async function chat() {
// Read user input from stdin.
const prompt = await new Promise<string>((resolve) => {
process.stdout.write("You: ");
process.stdin.once("data", (data) => resolve(data.toString().trim()));
});
// Build the payload – Bedrock expects a `messages` array.
const payload = {
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: prompt },
],
max_tokens: 1024,
temperature: 0.6,
};
// Sign and send the request.
const signedReq = await buildSignedRequest(payload);
const response = await fetch(signedReq);
if (!response.ok) {
const err = await response.text();
throw new Error(`Bedrock error ${response.status}: ${err}`);
}
// Collect and print the assistant’s answer.
const printAnswer = collectAnswer();
await handleNdjsonStream(response, printAnswer);
}
// Helper that builds the answer string and logs it once finished.
function collectAnswer() {
let answer = "";
return (chunk: any) => {
if (chunk.type === "message") {
answer += chunk.content;
} else if (chunk.type === "end") {
console.log("\nAssistant:", answer.trim());
}
};
}
// Run the chatbot.
chat().catch((e) => console.error("Error:", e));
Walk‑through of the script
| Step | What happens | Why it matters |
|---|---|---|
| Read stdin |
process.stdin.once("data", …) captures the user’s line. |
Keeps the chatbot interactive without extra libraries. |
| Create payload | An object with messages, max_tokens, and temperature. |
Matches the shape Bedrock expects; you can tweak creativity with temperature. |
| Sign request |
buildSignedRequest adds the required Authorization header. |
AWS will reject unsigned calls – this is the security handshake. |
| Fetch | Native fetch streams the response. |
No need for the heavy @aws-sdk/client-bedrock-runtime. |
| Stream handling |
handleNdjsonStream splits on \n, parses each line, and feeds it to collectAnswer. |
Prevents the “unexpected token” parse error that occurs when treating the whole stream as one JSON object. |
| Print answer | When a chunk with type: "end" arrives, the accumulated text is shown. |
Gives the user a clean, final response. |
Key tip: If you see “Rate exceeded” errors, you’ve hit Bedrock’s token‑per‑minute limit. Slow down the loop or request a higher quota.
The Takeaway
-
Node 22 lets a TypeScript file run without a separate compile step – the
--experimental-strip-typesflag removes type syntax on the fly. -
Native
fetchworks with Bedrock; you only need a tiny SigV4 signer from@aws-sdk/signature-v4. -
Bedrock streams NDJSON, not a single JSON object; split on
\nand ignore blank lines to avoid parse errors. - Signing is essential – AWS uses SigV4 to verify that the request truly comes from you.
- Rate limits are token‑based, so monitor total tokens across all calls, not just request counts.
- A single script can provide a complete chatbot that you can run locally or package as a serverless function without any Lambda boilerplate.
Give it a try, tweak the prompt or model parameters, and you’ll see how quickly a production‑grade AI chat experience can be assembled with just a handful of lines of TypeScript. Happy coding!
Transparency notice
This article was written with the help of an AI system — Groq (GPT OSS 120B).
Published: 2026-09-09 · Primary focus: Bedrock
All code blocks are intended to be correct and runnable, but please verify them
against the official docs for the tools mentioned before using in production.Find an error? Drop a comment — corrections are always welcome.
Top comments (0)