# The 10 fundamentals still worth learning when AI writes the code

> Ten programming fundamentals that still matter when coding agents write the code: what each is, how an agent gets it wrong, and the free course that teaches it.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-09-26 | Topics: [Career](https://flaviocopes.com/tags/career/) | Canonical: https://flaviocopes.com/fundamentals-ai-era/

No one wants to learn to code anymore, and I understand why. I build with coding agents every day, and almost all the code I shipped since March 2026 came out of an agent. What I still use is the knowledge that lets me steer the agent, read what it produced, and catch what it got wrong. Learning to [code with AI](https://flaviocopes.com/ai/) is mostly that.

Here are the ten I would still study. For each: what it is, a real moment where an agent gets it wrong and only someone who knows the fundamental notices, and the free course that teaches it.

If you are brand new, read [Introduction to Programming](https://flaviocopes.com/introduction-to-programming/) and my [beginner tips](https://flaviocopes.com/tips-beginner-programmers/) first, pick a vibe coding tool like I suggest in [IDE, CLI, or vibe coding tool](https://flaviocopes.com/ide-cli-or-vibe-coding-tool/), and ship something small. Then come back to learn how it works.

## 1. HTTP: requests and responses

HTTP is the conversation between a browser (or any client) and a server. The client sends a request: a method like `GET` or `POST`, a URL, headers, maybe a body. The server answers with a status code like `200` or `404`, headers, and usually a body. That exchange is the line between [frontend and backend](https://flaviocopes.com/frontend-vs-backend/).

The methods have meaning. `GET` reads and must not change anything, because browsers prefetch links, crawlers follow them, and caches store the answers. Changes go through `POST`, `PUT` or `DELETE`.

Ask an agent for a way to remove a newsletter subscriber and you may get this:

```js
app.get('/subscribers/:id/delete', async (req, res) => {
  await db.run('DELETE FROM subscribers WHERE id = ?', req.params.id)
  res.redirect('/subscribers')
})
```

It works when you click it. Then a link checker or a crawler visits every URL it finds and your subscribers are gone. There is no bug you can see unless you know that `GET` has to be safe.

Status codes and headers are also evidence. When this site moved from Netlify to Cloudflare Pages in June 2026, the headers from `curl -I https://flaviocopes.com` told me the cutover had happened.

The free [HTTP course](https://flaviocopes.com/courses/http/) covers methods, status codes, headers, caching and cookies.

## 2. Git: commits, branches, and getting work back

Git stores snapshots of your project. Each commit is one snapshot with a message and a pointer to its parent, and a branch is a name pointing at a commit. Because every commit remembers its parent, you can get back any earlier version of a file. Work you committed is very hard to lose.

Work you did not commit is another story, and agents run Git with a lot of confidence: `git reset --hard` to clean up, `git checkout -- .` to discard a file they broke, `git push --force` to make the remote match. Each throws something away. Commits that drop off a branch can usually be found again, because a reset moves the branch pointer and deletes nothing:

```bash
git reflog
git reset --hard HEAD@{1}
```

Uncommitted changes discarded by `reset --hard` are gone.

I often have several agents in the same repository at once. One ran a stray `git reset`, and for a few minutes commits another session had just made were off the branch, until I put them back. My agent instructions now say: stage explicit paths, make a fresh commit, never reset, rebase or force-push while the tree is changing. A few [Git hooks](https://flaviocopes.com/git-hooks/) reject broken commits before they land.

The free [Git course](https://flaviocopes.com/courses/git/) goes from the first commit to branches, remotes and recovering from mistakes.

## 3. SQL and how data fits into tables

A relational database keeps data in tables. Each row is one thing, a user or an order, and each column one attribute. A relationship is a column in one table holding the id of a row in another. SQL is how you ask questions about that data: `SELECT` to read, `WHERE` to filter, `JOIN` to combine tables, an index to make a lookup fast.

The classic agent mistake is the N+1 query. Ask for the last 20 orders with the customer's name and it writes a loop:

```js
const orders = await db.all(
  'SELECT * FROM orders ORDER BY created_at DESC LIMIT 20'
)

for (const order of orders) {
  order.customer = await db.get(
    'SELECT name FROM customers WHERE id = ?',
    order.customer_id
  )
}
```

That is 21 queries. With 20 rows on your laptop it feels instant; with 500 rows and a database on another machine, the page takes seconds. It was one query:

```sql
SELECT orders.*, customers.name
FROM orders
JOIN customers ON customers.id = orders.customer_id
ORDER BY orders.created_at DESC
LIMIT 20
```

You spot the loop only if you know what a `JOIN` is for and that each query is a round trip.

Data modeling matters as much. Ask for tags on a post and an agent may store them as a comma-separated string in one column, the shortest path to a passing test. It works until you need "every post with this tag".

The free [SQL course](https://flaviocopes.com/courses/sql/) goes from what a table is to joins, indexes and safe queries from an application.

## 4. The terminal

The terminal is where you type a command and a shell runs a program. A command has a name, arguments and flags. Programs print output, exit with a code (`0` means success), and a pipe sends the output of one into the next. Every coding agent lives here, installing, building, testing and deploying with shell commands, so this is the language you share with it.

Knowing the shell lets you read the transcript and spot the dangerous line. Some commands only look (`ls`, `cat`, `git status`). Others change the machine (`rm -rf`, `npm install -g`, anything with `--clear` or `--force`).

In September 2026 I had a few read-only agents check a long list of Ghostty settings against the installed binary. One "tested" `ghostty +ssh-cache --clear` and wiped the SSH terminfo cache on my Mac. In a long transcript it looked like every other line. You only notice if you know what `--clear` does. My brief for audit agents now says: no state-mutating commands outside the repository.

The free [Shell Commands course](https://flaviocopes.com/courses/terminal/) covers navigation, permissions, pipes and processes.

## 5. How the browser turns HTML and CSS into pixels

A browser reads HTML and builds a tree of nodes, the DOM. Then it reads CSS, and when several rules target the same element the cascade decides: importance and origin first, then specificity, then source order. Every element is a box with content, padding, border and margin. Layout places the boxes and paint draws them.

Agent CSS usually looks fine in the screenshot; the bugs hide in the cascade.

This site has 220 browser tools, built in batches by parallel agents. Tailwind v4 applies its `space-y-*` spacing through `:where()` selectors, which have zero specificity. Several agents added a harmless-looking `margin: 0` reset to their tool's stylesheet. Any real selector beats zero specificity, so the reset won and the spacing between form fields collapsed. Seven tools shipped like that. The fix was deleting the reset, and finding it meant knowing that specificity, not source order, decided that fight.

The same knowledge catches an agent "fixing" an overflow with `!important`, or centering a button inside three nested wrappers.

The free [HTML course](https://flaviocopes.com/courses/html/) and [CSS course](https://flaviocopes.com/courses/css/) cover the DOM, the cascade, the box model, Flexbox and Grid.

## 6. Async JavaScript and the event loop

JavaScript runs on one thread. Anything slow, a network request, a file read, a timer, is handed to the runtime and your code moves on. When the result is ready, a callback goes into a queue and the event loop runs it once the call stack is empty. An `await` pauses that one function until its promise settles, and nothing else. More in [the JavaScript event loop](https://flaviocopes.com/javascript-event-loop/).

A missing `await` is the async bug I hit most often, and the code still runs.

I did this one myself, in the Cloudflare Pages Function that handles a course purchase on this site. The newsletter subscribe call was not awaited. A Worker can stop as soon as the response is sent, so the `fetch` was cancelled and buyers never got on the list. No error, no log line. If you know that nothing forces a runtime to wait for a promise nobody awaits, that is the line you look for first.

In React the same idea shows up as a race, in a `useEffect` that fetches results whenever `query` changes:

```jsx
useEffect(() => {
  fetch(`/api/search?q=${query}`)
    .then(res => res.json())
    .then(setResults)
}, [query])
```

Type "git" fast and three requests go out. Whichever answers last wins, even if it was the one for "g". The fix is a cleanup that ignores stale responses:

```jsx
useEffect(() => {
  let ignore = false

  fetch(`/api/search?q=${query}`)
    .then(res => res.json())
    .then(data => {
      if (!ignore) setResults(data)
    })

  return () => {
    ignore = true
  }
}, [query])
```

You only ask for that cleanup if you know the earlier requests are still in flight.

The free [JavaScript course](https://flaviocopes.com/courses/javascript/) covers promises and the event loop, and the [React course](https://flaviocopes.com/courses/react/) covers effects.

## 7. Security basics: secrets, headers, sessions

Security basics are a short list of questions. Who sent this request? What may they do? How does the browser remember them? What may the page load? Where do the secrets live? And treat every input as hostile until validated.

Two agent mistakes keep coming back.

The first is the secret in the repository. An agent debugging a failing API call reads `.env`, pastes the key into a quick script "just for testing", and the next `git add .` commits it. Once a key is in Git history, deleting the line does nothing; you revoke it. When I moved my software to public GitHub repositories in September 2026, I ran gitleaks over the full history of every repository, before publishing and again after.

The second is a CSP that blocks its own script. You ask for a Content Security Policy, the agent adds `script-src 'self'`, and the page's own inline `<script>` stops running, along with every `onclick`. The console says why, if you read it. The tempting fix is `'unsafe-inline'`, which turns the protection off; the right one is a nonce or a hash, covered in [Content Security Policy explained](https://flaviocopes.com/content-security-policy/) along with the rest of the [security headers](https://flaviocopes.com/http-security-headers/).

When an agent offers to write its own password hashing, say no and point it at a maintained library, or at [passkeys](https://flaviocopes.com/passkeys-webauthn/).

The free [Security Fundamentals course](https://flaviocopes.com/courses/security-fundamentals/) covers threat modeling, secrets, and testing the failure paths that matter.

## 8. DNS and a little networking

DNS turns a name into an address. When you type `flaviocopes.com`, a resolver asks the authoritative name servers for the record and caches the answer for as long as the record's TTL says. Underneath, packets move between IP addresses and ports, and TCP makes delivery reliable. What you cannot skip: name resolution is cached, in layers, everywhere.

The agent mistake here is "DNS propagation". Ask it to move a site to a new server: it updates the A record, runs `dig`, sees the new IP and reports done. Visitors keep hitting the old server for up to the old TTL, often a whole day, because their resolvers cached the old answer. Shut the old server down too and visitors get connection errors for a day. Lower the TTL first, wait out the old one, then switch.

When Codex migrated my newsletter server between two DigitalOcean droplets in 2026, it tested the site on the new machine before touching public DNS, by forcing name resolution to the new IP. The `curl` version of that trick:

```bash
curl --resolve flaviocopes.com:443:203.0.113.10 https://flaviocopes.com/
```

Even so, right after the cutover my browser still had the old IP cached, the old Apache had just been stopped, and I got connection refused. Browsers cache too. The agent turned the old server into a temporary proxy to the new one.

The free [DNS course](https://flaviocopes.com/courses/dns/) covers records, TTLs and changing providers safely, and [Networking Foundations](https://flaviocopes.com/courses/networking-foundations/) covers IP, TCP and ports.

## 9. Reading code and debugging it

Reading code is following a value from where it enters to where it leaves: route, function, query, response. Debugging is that same walk with evidence: a stack trace says where the program was when it failed, logs say what it saw, and `git bisect` says which commit changed the behavior. The skill is turning "it doesn't work" into one precise question.

Agents fix what they can see. They rarely notice that the failure is not in your code.

On September 3, 2026, three Cloudflare builds of this site failed in a row with a missing `astro/_internal/logger` export, and nothing in the site had changed. An agent's instinct is to edit files until the error goes away. The trace pointed inside Astro itself, so the question became "what changed on the build machine?". This repository commits no lockfile, so every build installs the newest Astro that matches `package.json`, and 7.3.0 had been on npm for about three hours, broken. Raising the floor to `^7.3.1` fixed it.

To get there you have to read the trace instead of pasting it to the agent with "fix this", and know what a semver range does at install time.

TypeScript belongs here too. Types are documentation the compiler checks, and with `strict` on (see [Understanding tsconfig.json](https://flaviocopes.com/tsconfig-explained/)) an agent cannot quietly pass `undefined` where a string was expected. When it reaches for `as any` or `// @ts-ignore`, that is the line to read.

The free [Practical Debugging course](https://flaviocopes.com/courses/practical-debugging/) covers stack traces, logging, the web request path and debugging production safely.

## 10. Testing: what a good test proves

A test sets up a situation, runs the code, and checks one claim about the result. The good ones prove something you care about: "a duplicate webhook does not send a second email". The bad ones prove the code does what the code does: mock the dependency, call the function, assert the mock was called. Both are green.

Agents write a lot of the second kind, and if the code is built on a wrong assumption, the tests share it and pass.

Paddle, the payment provider I use for my courses, sends two separately signed deliveries per purchase: the fulfillment webhook and an account-level alert. For a while my `/purchase` function read the product id from either payload, handled both, and buyers got the welcome email twice. A happy-path test would have passed. The one that catches it, "the account alert payload sends nothing", needs you to know there are two payloads.

My rule when an agent hands me a test file: at least one case has to fail when I break the code on purpose, or it proves nothing. The tests that matter also run in CI, because a local hook is the one thing an agent can bypass.

The free [Testing JavaScript Applications course](https://flaviocopes.com/courses/testing/) goes from pure functions to APIs, databases, browsers and CI.

## How I study these now

I do not study these from a book anymore. I study them in the code the agents write for me.

The loop is in [Use AI to understand code](https://flaviocopes.com/understand-code-with-ai/). I point the agent at existing code and ask one sharp question at a time. What happens when this webhook arrives twice? If this returns 200, did the user get access? I want file paths with every answer, and assumptions kept apart from confirmed behavior. Then I check: send the request, read the header, open the row in the database.

I read the files, not the summary, because "done" in a report is not proof. One task at a time, on my own machine, so I can see what the agent touches. The longer story is in [what I learned in six months of agentic AI](https://flaviocopes.com/agentic-ai-lessons/).

And try first: read the function, guess what it does, then compare your guess with the agent's answer. That is how you build a feel for when the answer is nonsense.

I still don't know if writing code by hand is worth my time ([AI and the joy of programming](https://flaviocopes.com/ai-and-the-joy-of-programming/)). Understanding it is a different question. Software is cheap to produce now and [more people than ever are building it](https://flaviocopes.com/when-more-people-build-software/), but [its value was never in the hours](https://flaviocopes.com/software-was-never-worth-the-hours/). It is in whether the thing works.

If you want one place to start, take HTTP and Git. You will use both in the first hour with any agent.
