October 18, 2026

How I Built My First Full-Stack App (And What I Learned)

I share the step‑by‑step journey of turning a simple idea into a full‑stack web app, from planning and UI design to wiring up a Node.js API and deploying it live.

How I Built My First Full-Stack App

The Idea

It all started when I wanted a personal to‑do list that could sync across my phone and laptop without relying on a third‑party service. I sketched a few screens on a napkin, imagined a clean React UI, and thought, "Why not build it myself?" The excitement of creating something from scratch was the spark I needed to dive into full‑stack development.

Choosing the Stack

I wanted a stack that was both beginner‑friendly and powerful enough for production. After a quick Google search and a chat with a mentor, I settled on:

  • Frontend: React with Vite for lightning‑fast hot reloads.
  • Backend: Node.js with Express – simple, minimal, and JavaScript everywhere.
  • Database: SQLite for local development, later swapped to PostgreSQL on the cloud.
  • Deployment: Render (free tier) for both the API and the static site.

Setting Up the Project

First, I created two folders – client and server. In the terminal I ran:

# Create React app with Vite
npm create vite@latest client -- --template react
cd client
npm install

# Set up Express server
cd ..
mkdir server && cd server
npm init -y
npm install express cors sqlite3

I added a basic package.json script to start both parts concurrently using concurrently:

{
  "scripts": {
    "dev": "concurrently \"npm run dev --prefix client\" \"npm run start --prefix server\""
  },
  "devDependencies": {
    "concurrently": "^8.0.0"
  }
}

This tiny setup let me run npm run dev from the root and see both the React dev server and the Express API live.

Building the Frontend

In client/src/App.jsx I wrote a simple component that fetched todos from the API and displayed them:

import { useEffect, useState } from "react";

function App() {
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState("");

  useEffect(() => {
    fetch("/api/todos")
      .then(res => res.json())
      .then(setTodos);
  }, []);

  const addTodo = async () => {
    const res = await fetch("/api/todos", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ text: newTodo })
    });
    const created = await res.json();
    setTodos([...todos, created]);
    setNewTodo("");
  };

  return (
    <div className="container">
      <h1>My Todo List</h1>
      <ul>
        {todos.map(t => (
          <li key={t.id}>{t.text}</li>
        ))}
      </ul>
      <input value={newTodo} onChange={e => setNewTodo(e.target.value)} placeholder="New todo" />
      <button onClick={addTodo}>Add</button>
    </div>
  );
}

export default App;

Notice the relative /api path – Vite’s proxy feature will forward those calls to the Express server during development.

Creating the Backend API

Inside server/index.js I set up a tiny Express app:

const express = require("express");
const cors = require("cors");
const sqlite3 = require("sqlite3");
const path = require("path");

const app = express();
app.use(cors());
app.use(express.json());

// Initialize SQLite DB
const db = new sqlite3.Database(path.join(__dirname, "todos.db"));

db.run("CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT)");

app.get("/api/todos", (req, res) => {
  db.all("SELECT * FROM todos", [], (err, rows) => {
    if (err) return res.status(500).json({ error: err.message });
    res.json(rows);
  });
});

app.post("/api/todos", (req, res) => {
  const { text } = req.body;
  db.run("INSERT INTO todos (text) VALUES (?)", [text], function (err) {
    if (err) return res.status(500).json({ error: err.message });
    res.json({ id: this.lastID, text });
  });
});

const PORT = process.env.PORT || 4000;
app.listen(PORT, () => console.log(`Server listening on ${PORT}`));

That was enough to get CRUD operations working locally. I also added a tiny error‑handling middleware later, but the core logic stayed under 50 lines.

Connecting Frontend and Backend

During development Vite’s proxy option in vite.config.js made the connection seamless:

export default defineConfig({
  server: {
    proxy: {
      "/api": "http://localhost:4000"
    }
  }
});

When I built the React app for production (npm run build), the static files landed in client/dist. I then served those files from Express so the whole thing could run from a single Node process:

app.use(express.static(path.join(__dirname, "../client/dist")));
app.get("*", (req, res) => {
  res.sendFile(path.join(__dirname, "../client/dist/index.html"));
});

Now the API and the UI lived side‑by‑side, just like a real production app.

Deploying to the Cloud

Render made deployment painless. I created two services:

  1. Web Service – points to the server folder, runs node index.js, and automatically detects the PORT env variable.
  2. Static Site – points to the client folder, runs npm install && npm run build, and serves the dist folder. Both services were linked via a private network, so the frontend could call the backend using the internal URL Render provides. After a few minutes the app was live at https://my-todo-list.onrender.com.

Lessons Learned

  • Start Small: I kept the MVP to a single model (todos). Adding more features later felt natural instead of overwhelming.
  • Consistent Language: Using JavaScript on both client and server eliminated context‑switching and sped up debugging.
  • Environment Variables Matter: Storing the DB URL, API keys, and secret salts in .env files saved me from hard‑coding secrets.
  • Error Handling is Crucial: A missing try/catch in the API caused the whole app to crash once. Adding a global error handler made the system resilient.
  • Testing Early: A few Jest tests for the Express routes caught a typo before I pushed to production.

Conclusion

Building my first full‑stack app was a rollercoaster of excitement, frustration, and aha moments. By picking a simple stack, breaking the problem into bite‑size pieces, and leveraging modern tooling like Vite and Render, I turned a napkin sketch into a live product in under two weeks. If you’re on the fence about building your own full‑stack project, grab an idea that matters to you, follow the steps above, and remember: the best way to learn is by doing.

Discussion (0)

Please login to join the discussion.

Login

No comments yet. Be the first to share your thoughts!