ProgramGuru

Full-Stack TypeScript Application

Full-Stack TypeScript Application — purpose and prerequisites

A full-stack TypeScript application connects a typed browser client, a Node API, and a persistence layer through a clear contract. End-to-end type reuse reduces duplication, but input validation and database constraints still belong at their real boundaries. Start with a tiny domain contract and then add the working frontend and backend shown in the companion project.

What you’ll learn

  • Explain the purpose of full-stack typescript application.
  • Identify the typed contract, runtime responsibilities and common pitfalls.
  • Run the example and extend its behavior with a practical exercise.

How the idea works

The shared type describes the request and public response; the function implements trimming and a required-field rule. A full implementation adds HTTP transport, runtime JSON checks, persistence, and frontend fetch error handling.

Sharing an interface across frontend and backend does not authenticate requests or guarantee the database still enforces its invariants.

Visualize the flow

Full-Stack TypeScript Application — learning flowBrowser client → Typed Node API → Durable databaseFull-Stack TypeScript Application1Browser client2Typed Node API3Durable databaseProject core logic is executable; integrations may need setup.
A three-step view of the responsibilities and the observable result.

Run the checked TypeScript example

This source was compiled using TypeScript 5.8.3 with strict checking, then executed with Node.js 22.16.0. Its stdout below is the actual captured output. The core logic is runnable without a framework or a remote API; integration-specific examples appear in a separate lab when needed.

TypeScript source

type CreateNote = { text: string };
type Note = { id: number; text: string };
function createNote(request: CreateNote, id: number): Note {
  const text = request.text.trim();
  if (!text) throw new Error("Text required");
  return { id, text };
}
const note = createNote({ text: "  Build and test  " }, 1);
console.log(JSON.stringify(note));

Verified output

{"id":1,"text":"Build and test"}

Compile: npx tsc --strict --target es2022 --module commonjs full-stack-application.ts; run: node full-stack-application.js. For examples importing node: modules, install @types/node as a development dependency; for browser or React projects use the separate lab setup.

Trace the execution

The shared type describes the request and public response; the function implements trimming and a required-field rule. A full implementation adds HTTP transport, runtime JSON checks, persistence, and frontend fetch error handling.

Before editing the code, predict what the output will be for one changed input. Run it and compare your prediction with the actual result.

Build the practical integration

Integration companion: The primary example above is self-contained. The files below demonstrate a real framework, runtime, or project setup and may need additional packages or a browser.

shared.ts

export type CreateNote = { text: string };
export type Note = { id: number; text: string };

server.ts

import { createServer } from "node:http";
import type { CreateNote, Note } from "./shared";
const notes: Note[] = [];
const server = createServer(async (req, res) => {
  res.setHeader("content-type", "application/json");
  if (req.method === "GET" && req.url === "/notes") { res.end(JSON.stringify(notes)); return; }
  if (req.method === "POST" && req.url === "/notes") {
    let body = "";
    for await (const part of req) { body += String(part); if (body.length > 2048) { res.writeHead(413); res.end('{}'); return; } }
    try {
      const value: unknown = JSON.parse(body);
      if (typeof value !== "object" || value === null || !("text" in value) || typeof value.text !== "string" || !value.text.trim()) throw new Error("Invalid text");
      const data: CreateNote = { text: value.text.trim() };
      const note: Note = { id: notes.length + 1, text: data.text };
      notes.push(note); res.writeHead(201); res.end(JSON.stringify(note));
    } catch { res.writeHead(400); res.end(JSON.stringify({ error: "Invalid data" })); }
    return;
  }
  res.writeHead(404); res.end(JSON.stringify({ error: "Not found" }));
});
server.listen(3000, "127.0.0.1");

client.ts

import type { Note } from "./shared";
export async function loadNotes(): Promise<Note[]> {
  const response = await fetch("http://127.0.0.1:3000/notes");
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const data: unknown = await response.json();
  if (!Array.isArray(data) || !data.every(item => typeof item === "object" && item !== null && typeof item.id === "number" && typeof item.text === "string")) throw new Error("Bad response");
  return data as Note[];
}

Setup and execution

npm i -D typescript @types/node
npx tsc --strict --target es2022 --module commonjs server.ts client.ts shared.ts
node server.js
curl -X POST -H "Content-Type: application/json" -d '{"text":"Hello"}' http://127.0.0.1:3000/notes

The Node API and typed client share a contract but still validate incoming JSON independently. This starter has in-memory storage and does not yet include a browser UI or production persistence. Add those as project exercises.

Validation: This companion’s TypeScript source was also compiled successfully. Network-facing or browser interaction was not runtime-tested.

Common mistake and correction

Sharing an interface across frontend and backend does not authenticate requests or guarantee the database still enforces its invariants.

Determine whether the issue belongs to the type system, a trust boundary, or actual runtime behavior before choosing a correction.

Your turn: extend the example

Build a small notes app with a backend GET and POST API, a frontend form and list, validation, tests, and a persistent store.

Record expected success and failure outcomes, run the code, and compare the observed behavior. Keep the exercise compatible with strict type checking.

Check your understanding

Which statement correctly describes full-stack typescript application in the example?
Choose one answer. You can try again.

Answer and explanation

The shared type describes the request and public response; the function implements trimming and a required-field rule.

The shared type describes the request and public response; the function implements trimming and a required-field rule. A full implementation adds HTTP transport, runtime JSON checks, persistence, and frontend fetch error handling.

Key idea to remember

The shared type describes the request and public response; the function implements trimming and a required-field rule. A full implementation adds HTTP transport, runtime JSON checks, persistence, and frontend fetch error handling.

Apply the same reasoning to the practice task, then integrate it into a real app only when the runtime and dependency setup are ready.

Reaching the recap and completing the quizzes are separate milestones.

    Course overview →