cshttp Tutorial - Day 11: Talking to the Server Without Reloading (`fetch` + Plain Text)

Back to cshttp Tutorial Main Menu

Day 11: Talking to the Server Without Reloading (fetch + Plain Text)

Up to Day 10, every single time we sent data to the server (clicking a link or submitting an HTML <form>), the entire web page reloaded!

Modern web applications, however, often update just one small box on the screen without ever reloading the page: a chat message pops up, a "Saved!" status label appears, or a live counter updates smoothly.

How does a web page talk to the C# server in the background without reloading? By using JavaScript's built-in fetch() function!

Today, we will build a live Shared Notepad on our home page (/, /home, or /notepad). We will keep the communication as simple as possible: JavaScript and our C# server will pass pure plain text back and forth!

In Day 11, we will:

  1. Start with a clean, minimal Program.cs where /, /home, and /notepad all open our Shared Notepad page.
  2. Understand the difference between a Page Route (/notepad) and an API Data Route (/api/note).
  3. Create HandleNoteApi() so our server can send and receive raw plain text (Request.BodyText).
  4. Use fetch('/api/note') in JavaScript to load and display the note without reloading the page.
  5. Use fetch('/api/note', { method: 'POST', body: ... }) to save edited text back to the server in the background!

1. Two Kinds of Routes: Page Routes vs. API Routes

In our Shared Notepad app, our switch (path) statement will only need two main handlers (plus HandleNotFound()):

RouteWho calls it?What our C# server returns
/, /home, /notepadA human visitor opening the websiteA complete HTML page (text/html) containing the text boxes, buttons, and <script>
/api/noteJavaScript (fetch) running in the backgroundOnly the raw plain text of the note (text/plain) — zero HTML tags!

Why does /api/note start with /api/? In web development, a route that returns raw data for code (like JavaScript) instead of a full HTML page for humans is called an API endpoint. Naming it /api/note makes it easy to see at a glance which routes are pages and which routes are data endpoints!


2. Starting with Our Clean Skeleton

Let's start with a clean Program.cs that stores our shared noteText in server memory (static string noteText) and routes /, /home, and /notepad to HandleNotepad():

using System;
using cshttp;

namespace ConsoleApp1
{
    internal class Program
    {
        // NEW IN DAY 11: The shared notepad text stored in server memory!
        static string noteText = "Hello! This is the shared notepad.\nEdit me and click Save.";

        static void Main(string[] args)
        {
            WebApp.Run(8080, OnRequest);
        }

        static void OnRequest()
        {
            string path = Request.Path.ToLower();

            switch (path)
            {
                case "/":
                case "/home":
                case "/notepad":
                    HandleNotepad();
                    break;
                case "/api/note":
                    HandleNoteApi();
                    break;
                default:
                    HandleNotFound();
                    break;
            }
        }

        static void RenderPage(string title, string bodyContent)
        {
            string html = $@"<!DOCTYPE html>
<html>
    <head>
        <title>{title}</title>
    </head>
    <body>
        <nav>
            <a href='/'>Home (Notepad)</a> | 
            <a href='/api/note' target='_blank'>View Raw /api/note</a>
        </nav>
        <hr />

        {bodyContent}

        <hr />
        <footer>
            <small>Server Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}</small>
        </footer>
    </body>
</html>";

            Response.Write(html);
        }

        static void HandleNotFound()
        {
            Response.StatusCode = 404;
            string content = @"
                <h1>404 Not Found</h1>
                <p>The page that you are looking for does not exist.</p>";

            RenderPage("404 Not Found", content);
        }
    }
}

3. Building the /api/note Endpoint (HandleNoteApi)

Let's build HandleNoteApi() first! This method has two jobs depending on Request.HttpMethod:

  1. When Request.HttpMethod == "GET" (JavaScript asks for the current note):

    We set Response.ContentType = "text/plain"; and send noteText using Response.Write(noteText);.

  2. When Request.HttpMethod == "POST" (JavaScript sends updated note text to save):

    We read the raw text sent by the browser and store it in noteText!

How Does the Server Read Raw Plain Text (Request.BodyText)?

In Day 4, when we submitted an HTML <form>, the browser formatted the data with field names (sender=Alice&message=Hello), so we read it using Request.Form["sender"].

Today, JavaScript's fetch() is going to send pure, raw text in the request body—no form field names, no =, and no &:

Buy milk
Buy bread
Call grandma

Because it is not a form, Request.Form is empty (0 fields). Instead, how do we read the raw text body in cshttp?

You can read it in one single line using Request.BodyText!

string text = Request.BodyText;

Good to Know — Request.BodyText vs. Request.InputStream: Behind the scenes, Request.BodyText reads the raw bytes from Request.InputStream and decodes them as UTF-8 text automatically. If you ever see older C# code using new StreamReader(Request.InputStream, Encoding.UTF8).ReadToEnd(), Request.BodyText is cshttp's built-in one-line shortcut for that exact same operation!

Here is our complete HandleNoteApi() method:

        // NEW IN DAY 11: Sends or receives PLAIN TEXT. No HTML at all!
        static void HandleNoteApi()
        {
            // 1. POST: Read the raw text body sent by JavaScript and save it
            if (Request.HttpMethod == "POST")
            {
                string text = Request.BodyText;
                noteText = text;

                Console.WriteLine("Note saved (" + text.Length + " characters)");

                Response.ContentType = "text/plain";
                Response.Write("Saved! (" + text.Length + " characters)");
                return;
            }

            // 2. GET: Send the current note as plain text
            Response.ContentType = "text/plain";
            Response.Write(noteText);
        }

4. Building HandleNotepad() — Part 1: The HTML Elements

Now let's build the /notepad (and /home) page! First, let's look at the HTML elements we need on the page (before we add any JavaScript):

  1. <pre id='noteView'></pre>: A box to display the current note from the server (<pre> preserves spaces and line breaks).
  2. <button onclick='loadNote()'>Reload from server</button>: A button that calls a JavaScript function named loadNote() when clicked.
  3. <textarea id='noteEditor'></textarea>: A multi-line text box where the visitor can edit the note.
  4. <button onclick='saveNote()'>Save to server</button>: A button that calls a JavaScript function named saveNote() when clicked.
  5. <span id='status'></span>: A green label next to the Save button where JavaScript can show "Saved! (42 characters)".
<h1>Shared Notepad</h1>
<p>Everyone visiting this website shares this one note.</p>

<div style='background: #f9f9f9; border: 1px solid #ddd; padding: 15px; margin-bottom: 15px;'>
    <h2>1. The note (from the server)</h2>
    <pre id='noteView' style='background: white; border: 1px solid #ccc; padding: 10px; min-height: 60px;'></pre>
    <button onclick='loadNote()'>Reload from server</button>
</div>

<div style='background: #f9f9f9; border: 1px solid #ddd; padding: 15px; margin-bottom: 15px;'>
    <h2>2. Edit the note</h2>
    <textarea id='noteEditor' rows='6' style='width: 100%;'></textarea>
    <br /><br />
    <button onclick='saveNote()'>Save to server</button>
    <span id='status' style='color: green; margin-left: 10px;'></span>
</div>

Notice that there is no <form> tag anywhere! Instead, our buttons use onclick='loadNote()' and onclick='saveNote()' to trigger JavaScript functions directly.


5. Building HandleNotepad() — Part 2: Loading Text with fetch() (GET)

Now let's write the <script> tag that powers loadNote()!

First, we grab references to our three HTML elements by their id:

var noteView = document.getElementById('noteView');
var noteEditor = document.getElementById('noteEditor');
var statusText = document.getElementById('status');

Next, how does loadNote() ask our C# server for /api/note in the background?

function loadNote() {
    fetch('/api/note')
        .then(function (response) {
            return response.text(); // Step 1: Read the server's reply as plain text
        })
        .then(function (text) {
            // Step 2: Put the text into our <pre> box and <textarea> editor!
            noteView.textContent = text;
            noteEditor.value = text;
        });
}

Let's Trace Those 3 Steps in loadNote():

  1. fetch('/api/note'): Sends a background GET request to http://localhost:8080/api/note.
  2. .then(function (response) { return response.text(); }): When the server replies, response.text() extracts the plain text from the reply.
  3. .then(function (text) { ... }):
    • noteView.textContent = text; puts the text inside the <pre id='noteView'> box. (Using .textContent in JavaScript is 100% safe against HTML injection—just like WebApp.HtmlEncode in C#!)
    • noteEditor.value = text; fills the <textarea id='noteEditor'> box with the same text so you can start editing it right away!

6. Building HandleNotepad() — Part 3: Saving Text with fetch() (POST)

Finally, what happens when the visitor edits the <textarea> and clicks Save to server (saveNote())?

To make fetch() send a POST request instead of a GET request, we pass a second parameter { method: 'POST', body: noteEditor.value }:

function saveNote() {
    fetch('/api/note', {
        method: 'POST',
        body: noteEditor.value // Send the textarea's text as the raw request body!
    })
        .then(function (response) {
            return response.text();
        })
        .then(function (reply) {
            statusText.textContent = reply; // Show "Saved! (42 characters)"
            loadNote();                     // Refresh the <pre> box with the saved note
        });
}

Here is a visual diagram of how JavaScript in the browser and C# on the server talk to each other without ever reloading the page:

 Browser (JavaScript)                          C# Server (HandleNoteApi)
 ────────────────────                          ─────────────────────────
 1. loadNote():
    fetch('/api/note')            ── GET ───▶  Response.Write(noteText)
    noteView.textContent = text   ◀── text ──

 2. saveNote():
    fetch('/api/note', {
        method: 'POST',
        body: noteEditor.value    ── POST ──▶  noteText = Request.BodyText
    })                                         Response.Write("Saved! ...")
    statusText.textContent = ...  ◀── reply ─

7. Complete Code for Day 11

Here is the complete, self-contained Program.cs for Day 11:

using System;
using cshttp;

namespace ConsoleApp1
{
    internal class Program
    {
        // NEW IN DAY 11: The shared notepad text stored in server memory
        static string noteText = "Hello! This is the shared notepad.\nEdit me and click Save.";

        static void Main(string[] args)
        {
            WebApp.Run(8080, OnRequest);
        }

        static void OnRequest()
        {
            string path = Request.Path.ToLower();

            switch (path)
            {
                case "/":
                case "/home":
                case "/notepad":
                    HandleNotepad();
                    break;
                case "/api/note":
                    HandleNoteApi();
                    break;
                default:
                    HandleNotFound();
                    break;
            }
        }

        static void RenderPage(string title, string bodyContent)
        {
            string html = $@"<!DOCTYPE html>
<html>
    <head>
        <title>{title}</title>
    </head>
    <body>
        <nav>
            <a href='/'>Home (Notepad)</a> | 
            <a href='/api/note' target='_blank'>View Raw /api/note</a>
        </nav>
        <hr />

        {bodyContent}

        <hr />
        <footer>
            <small>Server Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}</small>
        </footer>
    </body>
</html>";

            Response.Write(html);
        }

        // NEW IN DAY 11: Sends or receives PLAIN TEXT. No HTML at all!
        static void HandleNoteApi()
        {
            if (Request.HttpMethod == "POST")
            {
                // Read the raw text body sent by JavaScript's fetch()
                string text = Request.BodyText;
                noteText = text;

                Console.WriteLine("Note saved (" + text.Length + " characters)");

                Response.ContentType = "text/plain";
                Response.Write("Saved! (" + text.Length + " characters)");
                return;
            }

            // GET: send the current note as plain text
            Response.ContentType = "text/plain";
            Response.Write(noteText);
        }

        // NEW IN DAY 11: The Notepad page. JavaScript talks to /api/note without reloading the page!
        static void HandleNotepad()
        {
            // Notice we use a normal @"..." string (without $), so JavaScript's { } braces don't need doubling!
            string content = @"
                <h1>Shared Notepad</h1>
                <p>Everyone visiting this website shares this one note.</p>

                <div style='background: #f9f9f9; border: 1px solid #ddd; padding: 15px; margin-bottom: 15px;'>
                    <h2>1. The note (from the server)</h2>
                    <pre id='noteView' style='background: white; border: 1px solid #ccc; padding: 10px; min-height: 60px;'></pre>
                    <button onclick='loadNote()'>Reload from server</button>
                </div>

                <div style='background: #f9f9f9; border: 1px solid #ddd; padding: 15px; margin-bottom: 15px;'>
                    <h2>2. Edit the note</h2>
                    <textarea id='noteEditor' rows='6' style='width: 100%;'></textarea>
                    <br /><br />
                    <button onclick='saveNote()'>Save to server</button>
                    <span id='status' style='color: green; margin-left: 10px;'></span>
                </div>

                <script>
                    var noteView = document.getElementById('noteView');
                    var noteEditor = document.getElementById('noteEditor');
                    var statusText = document.getElementById('status');

                    // 1. Ask the server for the note (GET), then show it
                    function loadNote() {
                        fetch('/api/note')
                            .then(function (response) {
                                return response.text();   // read the reply as plain text
                            })
                            .then(function (text) {
                                noteView.textContent = text;  // textContent = safe, shown exactly as-is
                                noteEditor.value = text;
                            });
                    }

                    // 2. Send the textarea's text to the server (POST), then reload it
                    function saveNote() {
                        fetch('/api/note', {
                            method: 'POST',
                            body: noteEditor.value          // the raw text IS the request body
                        })
                            .then(function (response) {
                                return response.text();
                            })
                            .then(function (reply) {
                                statusText.textContent = reply;  // e.g. 'Saved! (42 characters)'
                                loadNote();
                            });
                    }

                    // Load the note automatically as soon as the page opens
                    loadNote();
                </script>";

            RenderPage("Shared Notepad", content);
        }

        static void HandleNotFound()
        {
            Response.StatusCode = 404;
            string content = @"
                <h1>404 Not Found</h1>
                <p>The page that you are looking for does not exist.</p>";

            RenderPage("404 Not Found", content);
        }
    }
}

8. Test It in Your Browser

  1. Run the application (dotnet run or press Start in Visual Studio) and visit http://localhost:8080/.
  2. First, click View Raw /api/note in the navigation bar (or visit http://localhost:8080/api/note). Notice that you see only the raw text of the note—no HTML, no navigation bar, and no footer! That is what fetch('/api/note') reads behind the scenes.
  3. Go back to http://localhost:8080/. Look at the Server Time in the footer at the bottom of the page, and remember the seconds shown on the clock.
  4. Now type a shopping list into the 2. Edit the note box:
     Buy milk
     Buy bread
     Call grandma
  5. Click Save to server.
  6. Look closely at what happened:
    • The green label says Saved! (31 characters).
    • Box 1 (The note from the server) immediately updated to show your shopping list!
    • Now look at the Server Time clock in the footer at the bottom: the time did NOT change! That proves the entire page never reloaded—JavaScript updated the note in the background using fetch()!
  7. Open a second browser window at http://localhost:8080/—your shopping list is right there too, shared across everyone visiting the server!

9. Quick Reference Summary

GoalCode
C# Server: Send plain textResponse.ContentType = "text/plain";
Response.Write(noteText);
C# Server: Read raw plain text bodystring text = Request.BodyText;
JavaScript: Request plain text (GET)fetch('/api/note').then(function(r) { return r.text(); })
JavaScript: Send plain text (POST)fetch('/api/note', { method: 'POST', body: text })
JavaScript: Safely display text in HTMLelement.textContent = text; (prevents HTML/XSS injection!)

In Day 12, we will learn how to send structured objects and lists (like a list of comments where each item has a separate name, message, and time) between C# and JavaScript using JSON!