Part 0 — Orientation

A Programming Foundations for Building Real Systems

This chapter is the ground floor. By the end of it you will know what a program is, what the terminal does, how a browser talks to a server, what a database is for, and how to keep your work safe with version control — with no gaps papered over. Everything in the thirty-one chapters that follow assumes exactly this much and nothing more.

In this chapter13 sections · about 42 min
  1. What you need to know first
  2. Why this chapter exists
  3. What a program actually is
  4. Files, folders, and paths
  5. The terminal: what it is and why professionals live in it
  6. JavaScript, Node.js, and TypeScript
  7. Packages, npm, and the true cost of a dependency
  8. Client and server: what happens when you click a button
  9. What a database is, and why files won't do
  10. Version control: git, and why your laptop is not the product
  11. Deploying, production, and environments
  12. Reading errors and debugging
  13. How to use this book

What you need to know first

No programming. That is the whole prerequisite. If you have never opened a terminal, never written a line of code, and are not sure what a server is, you are the reader this chapter was written for. Everything is defined the first time it appears.

What you do need is practical: a laptop running macOS, Windows or Linux that you are allowed to install software on, an internet connection, and a few unhurried hours. No math beyond arithmetic. No paid tools: every piece of software in this chapter is free.

You also need six words of apparel vocabulary, because the examples use them from here on.

  • ERP stands for Enterprise Resource Planning. A grand name for a very ordinary idea: the software a company uses to track the things it makes, the things it has, and the money it is owed.
  • Style: one design. "The Boxy Pocket Tee" is a style. It has a style code, like SS26-TEE-01.
  • SKU: Stock Keeping Unit, said "skew". One specific size and color of a style: the medium in black is one SKU, the large in black is another. You count and ship SKUs. You design and price styles.
  • Inventory (or on hand): how many units of a SKU are physically in the warehouse right now.
  • ATS: Available-to-Sell. How many units you can actually promise a customer today: what is on hand, minus what is already promised to somebody else. It is the number the whole system exists to get right.
  • Wholesale order: a shop or chain buying from the brand to resell. One order can be hundreds of units across dozens of SKUs, agreed months before it ships.

Why this chapter exists

You are going to build an ERP for an apparel brand: styles, SKUs, inventory, ATS, wholesale orders, and invoices.

Real business systems have a property toy projects do not: somebody is depending on the number being right. If your app says 200 shirts are available and only 40 exist, a buyer gets a canceled order and your client loses an account.

Most tutorials skip this material. They say "open your terminal and run npm install" and move on, and you either knew what that meant or you spent four hours confused and feeling stupid. You are not stupid. Nobody told you. So here is everything, told properly. Type the commands rather than reading them, because your hands learn things your eyes do not.

What a program actually is

A computer follows instructions very fast and completely literally. It has no judgment and no ability to fill in gaps. A program is a list of instructions written down for that machine.

One distinction matters more than any other in this chapter, and beginners rarely have it straight. Source code and a running process are two separate things.

  • Source code is text sitting in a file on a disk. It is inert — a recipe on a card. You can read it, email it, put it in a drawer. Nothing happens.
  • A process is what exists when the computer is actually doing the instructions. It occupies memory, has a number (a process ID), and can be busy, stuck or crashed. It is the meal being cooked, and it disappears when you stop it.

"Running code" means handing your recipe to something that knows how to follow it, and watching it cook. Here it is, start to finish.

$ mkdir hello
$ cd hello
$ echo 'console.log("Ready to ship " + 24 + " units");' > run.js
$ node run.js
Ready to ship 24 units

Walkthrough. The $ is the prompt: the terminal saying "I'm listening." It is already on the screen, so you type only what comes after it. mkdir hello makes a folder, and cd hello moves into it.

The echo ... > run.js line writes one line of text into a new file, where > means "send the output into this file instead of showing it to me." At that point run.js is source code, doing nothing.

Then node run.js starts a process: Node reads the file, follows the instruction, prints a line, exits. The process lived a few milliseconds and is gone. The file is still there, unchanged.

Core principle

Source code is the recipe. A process is the cooking. Most confusing bugs in your first year come from mixing these up: editing the recipe and wondering why the meal on the table did not change. When you change a file, the already-running process does not know. Restart it, or use a tool that restarts it for you.

Files, folders, and paths

Everything you write lives in files. A file is a named lump of data on a disk. A folder (also called a directory) is a container for files and other folders, nesting as deep as you like, so the whole structure is a tree. A path is the address of a file: the route through that tree.

  • An absolute path starts at the top of the tree and spells out every step. On macOS and Linux it begins with /. Example: /Users/you/erp/apps/web/package.json. It is a full street address including the country, and means the same thing wherever you stand.
  • A relative path starts from wherever you currently are: apps/web/package.json, or ../shared/types.ts. It is "two doors down on the left," useful only if you know where you're standing.

Two special names show up constantly: . means "this folder," .. means "the folder above," so ../.. is "up two levels." And ~ is shorthand for your home folder, on a Mac /Users/yourname.

$ pwd
/Users/you/erp
$ ls
apps          package.json  packages      turbo.json
$ ls apps/web/src
app  components  lib
$ cd apps/web/src/lib
$ pwd
/Users/you/erp/apps/web/src/lib
$ cd ../../../..
$ pwd
/Users/you/erp

Walkthrough. pwd means "print working directory." It answers "where am I?" and is the first thing to run when you are lost. ls lists the current folder, and ls apps/web/src lists another folder without moving into it. cd changes directory, and cd ../../../.. climbs four levels: lib to src to web to apps to the project root. That relative path only made sense because we knew where we were standing.

Two names in that listing belong to the project we are going to build. apps and packages hold several related projects side by side in one repository (a layout called a monorepo), and turbo.json configures Turborepo, the tool that runs commands across all of them at once. Chapter 10 sets this up properly. For now it is just a folder tree.

What a text file really is

Your source code is a plain text file, and that phrase hides something, because "text" is a lie the computer tells you kindly. A disk stores bytes (numbers from 0 to 255), and a file is just an ordered pile of them.

To turn bytes into letters you need an encoding: an agreed table mapping numbers to characters. Always use UTF-8. It can represent every character in every language and is the default nearly everywhere.

$ printf 'Hi\n' > note.txt
$ xxd note.txt
00000000: 4869 0a                                  Hi.

$ printf 'caf\xc3\xa9\n' > cafe.txt
$ cat cafe.txt
café
$ wc -c cafe.txt
       6 cafe.txt

Walkthrough. xxd shows a file's raw bytes in hexadecimal, a numbering system that lines up neatly with bytes. note.txt is three bytes: 48 is H, 69 is i, and 0a is the newline, the invisible byte that ends a line. That is all a "line" is.

The second file shows why encoding matters: café looks like four characters, but wc -c (word count, counting bytes) reports six — c, a, f, then two bytes (c3 a9) for the single character é, then the newline. In UTF-8 most characters are one byte and some are two, three or four.

This matters the day a customer name contains é and something reports the wrong length or stores mojibake — garbled characters like café. When you see that, you will know instantly that two pieces of software disagreed about the encoding.

A "text file" also has no inherent type. run.js, styles.sql and package.json are the same kind of thing: readable bytes. The extension is a hint, nothing more.

The terminal: what it is and why professionals live in it

The terminal (also "command line," "shell," or CLI, for Command Line Interface) is a window where you type instructions to your computer as text, one line at a time, and it types back.

Beginners find this hostile: it looks like 1983. Professionals live in it because it is:

  • precise ("click the third icon" is ambiguous, rm build/cache.json is not);
  • repeatable (a command can be saved, shared, or run by a robot at 3am, mouse clicks cannot);
  • composable (small tools plug into each other, so you can build things nobody wrote a button for);
  • complete (databases, package managers and deployment systems all have graphical wrappers, but the button versions are always a subset of the command line);
  • and honest (graphical tools show a spinner and "Something went wrong," while the terminal shows the actual error).

The ten commands that carry the weight

Two words in the table below arrive before their turn. Your API (Application Programming Interface) is the set of web addresses your own program answers on: how other programs, including the pages in your own browser, ask it for data. Your development server, usually shortened to dev server, is the copy of the application running on your laptop while you work on it.

Learn these ten and you can function at a command line. Everything else you can look up the day you need it.

CommandStands forWhat it doesYou'll use it to…
cdchange directoryMoves you to another folderGet into your project before running anything
lslistShows what is in a folderConfirm a file was actually created
mkdirmake directoryCreates a folderSet up a new part of the project
catconcatenatePrints a file's whole contentsRead a config file fast
grepglobal regular expression printSearches text for a patternFind every place a function is used
git(a name, not an acronym)Version controlSave snapshots of your work
npmjavascript package managerInstalls and runs project toolingAdd libraries, start the dev server
psqlPostgreSQL clientTalks to your database directlyCheck what the data really says
curlclient for URLsSends an HTTP request from the terminalTest your API without a browser
tailtail (end of a file)Shows the last lines of a file, liveWatch logs while you click around

Two of those names get mangled in conversation. People often expand npm as "node package manager." Its own manual page calls it simply "javascript package manager," and the description underneath reads "the package manager for the Node JavaScript platform." And curl is a play on "Client for URLs," which its FAQ notes also reads as "see URL."

Watching the commands do real work

Now put several of them on a real project.

$ mkdir -p apps/web/src/lib
$ ls -la apps/web/src
total 0
drwxr-xr-x  4 you  staff  128 Jul 25 15:41 .
drwxr-xr-x  6 you  staff  192 Jul 25 15:40 ..
drwxr-xr-x  8 you  staff  256 Jul 25 15:39 app
drwxr-xr-x  3 you  staff   96 Jul 25 15:41 lib

Walkthrough. mkdir -p creates the whole chain of folders at once, including parents that don't exist yet. Without it the command fails because apps is missing. The -p is a flag, an option that modifies behavior. ls -la uses two: -l for the long listing, -a for "all," including hidden entries. The d starting each permissions string means "this is a directory."

$ cat apps/web/src/lib/inventory.ts
export function availableToSell(
  onHand: number,
  reserved: number,
): number {
  return onHand - reserved;
}

$ cd apps/web/src
$ grep -rn "availableToSell" .
./lib/inventory.ts:1:export function availableToSell(
./app/api/ats/route.ts:9:import { availableToSell } from "@/lib/inventory";
./app/api/ats/route.ts:22:  const ats = availableToSell(onHand, reserved);

Walkthrough. cat dumps a file without opening an editor. grep searches for text. -r means "recursive," look inside every folder underneath. -n means "show line numbers." The . at the end means "start here, in this folder." The output format is file:line:matching line.

In three seconds we learned where availableToSell is defined and every place it is used. When you inherit a codebase, this is how you read it.

$ tail -f apps/web/dev.log
2026-07-25 16:03:58  GET  /api/ats?styleCode=SS26-TEE-01  200 in 41ms
2026-07-25 16:04:11  POST /api/orders                     201 in 88ms
2026-07-25 16:04:12  GET  /api/orders/ord_88f31           200 in 19ms
^C

Walkthrough. tail shows the end of a file, and -f means "follow," so it stays open and prints new lines as they arrive. A log is just a text file your program appends a line to whenever something happens, so this is how you watch your application think: keep it running in one window, click around in the browser, see each request appear live.

The ^C shows when you press Control-C to stop a command — your escape hatch from almost any stuck process.

Practical habit

Keep two or three terminal tabs open at all times: one running your development server, one tailing logs, one free for commands. And press the up arrow to recall your last command, Tab to auto-complete a half-typed file name. Those keys save hours per week.

JavaScript, Node.js, and TypeScript

These three names get used interchangeably by people who should know better. They are three different things.

JavaScript is a language

A programming language is a set of rules for writing instructions: what counts as a valid sentence, and what each sentence means. JavaScript is one such language, created at Netscape in 1995 to make web pages interactive, and it is still the language developers report using most — 66% of all respondents in Stack Overflow's 2025 developer survey, ahead of SQL and Python.

By itself the language does nothing. It is a set of grammar rules on paper, and you need something that can read those rules and act on them.

A runtime is the thing that actually runs it

A runtime is a program that reads your source code and performs the instructions, plus the surrounding facilities your code needs: a way to read files, a way to talk over the network, a clock. Two runtimes matter to us.

  • The browser (Chrome, Safari, Firefox) can draw on screen and respond to clicks. It deliberately cannot read arbitrary files on your hard drive, because that would be a catastrophic security hole.
  • Node.js took the JavaScript engine out of Chrome (an engine called V8) and wrapped it in a program you run from the terminal. Node can read files, listen for network connections and talk to a database. It cannot draw anything, because no screen is involved.

Same language, two very different environments. Code that works in Node may be meaningless in a browser and vice versa, which explains much of the confusion beginners feel early on.

$ node --version
v24.18.0
$ node
Welcome to Node.js v24.18.0.
Type ".help" for more information.
> 2 + 2
4
> const sizes = ["S", "M", "L"];
undefined
> sizes.length
3
> .exit
$

Walkthrough. node --version asks Node to report itself, and "command not found" means it is not installed. Typing node alone opens a REPL (Read Eval Print Loop), a scratchpad where each line runs immediately and prints its result. Note that declaring const sizes printed undefined: a declaration produces no value.

Node ships two release lines at once. The Current line gets new features and changes fast. The LTS line (Long Term Support) is the one kept stable and patched for years, and it is the one to install. As of July 2026 the Active LTS releases are Node 24 (24.18.0) and Node 22, and Node 26 is Current. Your version numbers will be higher than the ones printed above, and that is fine.

TypeScript is JavaScript with a safety inspector

JavaScript lets you do anything. It will happily add a number to a piece of text, produce nonsense, and carry on cheerfully. That is fine for a ten-line script and dangerous for a system deciding how many shirts to ship.

A type is a promise about what kind of value something is: a number, a piece of text (a "string"), a true/false (a "boolean"), a list, an object with particular fields. TypeScript is JavaScript plus a way to write those promises down, plus a checker that reads your whole codebase and tells you where they are broken — before you run anything. Here is the kind of bug it saves you from, first in plain JavaScript:

// orders.js
function totalUnits(lines) {
  let total = 0;
  for (const line of lines) {
    total += line.quantity;
  }
  return total;
}

// Two order lines. The second quantity arrived from a web form,
// so it is text ("24"), not a number (24).
const lines = [
  { sku: "SS26-TEE-01-M", quantity: 12 },
  { sku: "SS26-TEE-01-L", quantity: "24" },
];

console.log(totalUnits(lines));
$ node orders.js
1224

Walkthrough. Read that number again: 1224. The correct answer is 36. In JavaScript + means two things: applied to numbers it adds, and applied to anything involving text it glues strings together.

So 0 + 12 gave 12, then 12 + "24" quietly converted the 12 into the text "12" and stuck "24" on the end, producing "1224". No error. No warning. And somewhere downstream your warehouse is now picking 1,224 shirts.

The reason the quantity was text is mundane: values submitted from a web form arrive as text, every field, always. Nothing exotic had to go wrong for a wrong number to reach the warehouse.

The same bug, caught before it runs

Now the same code in TypeScript, with the promises written down:

// orders.ts
type OrderLine = {
  sku: string;
  quantity: number;
};

function totalUnits(lines: OrderLine[]): number {
  let total = 0;
  for (const line of lines) {
    total += line.quantity;
  }
  return total;
}

const lines: OrderLine[] = [
  { sku: "SS26-TEE-01-M", quantity: 12 },
  { sku: "SS26-TEE-01-L", quantity: "24" },
];

console.log(totalUnits(lines));
$ npx tsc --noEmit orders.ts
orders.ts:17:27 - error TS2322: Type 'string' is not assignable
to type 'number'.

17   { sku: "SS26-TEE-01-L", quantity: "24" },
                             ~~~~~~~~

  orders.ts:4:3 - The expected type comes from property 'quantity'
  which is declared here on type 'OrderLine'
    4   quantity: number;
        ~~~~~~~~

Found 1 error in orders.ts:17

Walkthrough. type OrderLine = { ... } declares a shape: an order line has a sku that is text and a quantity that is a number. The annotations lines: OrderLine[] and : number say "a list of order lines in" and "a number out."

npx runs a command that lives inside your project's installed packages, so you do not have to install it across your whole machine. Here it runs tsc, the TypeScript compiler. --noEmit means "check the code but do not write any output files." The two long lines above are wrapped by the terminal, and the compiler prints each as one line.

Look at the quality of that error: file, line 17, column 27, the exact problem, the offending value underlined, and a pointer to where the promise was originally made (line 4). It found the bug in under a second, without running the program, without a database, without a warehouse.

Core principle

Types earn their keep. A type checker rereads every line of your code every time you save, checking that the shape of data flowing through the system matches what each piece expects. Most serious bugs in a business system are shape bugs: a missing field, a number that is secretly text, a value that might be absent. Types catch that whole class before the code ever runs, on every save, for as long as the project lives.

Compiling: why the annotations disappear

The annotations are not part of JavaScript, so something has to remove them before the code runs. Leave them in and the runtime hits words it has no rules for and stops. That removal step is called compiling.

A browser cannot run a .ts file at all. Node can run one directly. Since versions 22.18 and 23.6 it strips the type annotations out and runs what is left, a feature that became stable in Node 24.12. But as Node's own documentation puts it, "no type checking is performed."

So the checking is always a separate step, done by tsc or by your editor as you type. Your project tooling wires both up for you, and knowing they are separate explains why a file can pass Node and still be full of type errors, and why you sometimes see a .js file you never wrote.

Packages, npm, and the true cost of a dependency

Nobody writes everything themselves. A package (also called a library, a module, or a dependency) is a bundle of somebody else's code you can pull into your project. A package manager fetches those bundles, tracks which versions you have, and fetches the packages that those packages need. For JavaScript the standard one is npm, which installs alongside Node.

$ npm install zod

added 1 package, and audited 412 packages in 1s

142 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Walkthrough. We asked for one package, zod (a library for validating that incoming data has the shape you expect, and you will use it constantly in this book). npm downloaded it, then audited 412 packages. Four hundred and twelve.

Those are the packages already in the project plus everything they depend on: package managers install dependencies-of-dependencies automatically, and the tree gets deep fast. "Audited" means npm compared every one of them against a public list of known security problems, and "found 0 vulnerabilities" means none matched today.

Three artifacts appear in your project, and you should understand all three.

The first artifact: package.json

{
  "name": "apparel-erp-web",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "next": "^16.2.11",
    "react": "^19.2.8",
    "react-dom": "^19.2.8",
    "zod": "^4.4.3"
  },
  "devDependencies": {
    "typescript": "^7.0.2"
  }
}

Walkthrough. This is package.json, the identity card of a project. JSON stands for JavaScript Object Notation, a plain-text format of names and values, and the standard way programs exchange data.

scripts defines shortcuts: npm run dev means "run next dev," so everyone starts the app the same way. dependencies are packages the running application needs. devDependencies only matter while you are developing. The ^ means "this version or any later bug-fix or feature release, but not a new major version."

Those version numbers were the current releases in July 2026 — Next.js 16.2.11, React 19.2.8, Zod 4.4.3, TypeScript 7.0.2. They will have moved by the time you read this, and that is expected. What matters is the shape of the file, not the digits. Check the current version of anything with npm view <package> version.

node_modules and the lockfile

The second artifact is node_modules, the folder where the downloaded code physically lives:

$ du -sh node_modules
412M	node_modules
$ ls node_modules | wc -l
     387
$ cat .gitignore
node_modules
.next
.env.local

Walkthrough. du -sh is "disk usage, summarized, human-readable" — 412 megabytes on this particular project. Yours will differ, but hundreds of megabytes is ordinary. ls | wc -l pipes the listing into a line counter: 387 entries. The | is a pipe, feeding one command's output into the next.

Last is .gitignore, a list of things excluded from version control. node_modules is on it because it is huge and reproducible. You save the list of what you need, and anyone rebuilds the folder with npm install.

The third artifact is the lockfile (package-lock.json). package.json says "zod, version 4 or better" — a range that resolves differently on different days. The lockfile records the exact version of every package installed, plus a fingerprint of each. Commit it: it makes your machine, your colleague's and the production server run byte-identical code. Without it you get the worst sentence in software: "it works on my machine."

What a dependency actually costs

Installing a package is not free. You have accepted code you have not read running with your application's full permissions, a supply chain where a compromised maintainer account becomes your breach, an upgrade treadmill, and a piece of your architecture someone else controls. Before adding one, ask how many lines you would write yourself. Under about thirty? Write them.

Client and server: what happens when you click a button

Get this one idea straight and most of web development stops being mysterious. There are two computers: yours, and someone else's.

  • The client is the program in front of the user, normally a web browser. It draws things, listens for clicks and typing, and asks for information when it needs it. It runs on the user's machine, so the user fully controls it: they can read its code, change its values, and lie to it.
  • The server is a program running on a machine you control, somewhere else, all the time. It holds the real data, enforces the rules, and answers questions. "Server" describes a role rather than a special kind of box: your laptop becomes one the moment you run npm run dev.

They talk over HTTP (HyperText Transfer Protocol), a strict, simple conversation format. One side sends a request. The other sends back a response. That's it. Every page load, every button, every app on your phone: requests and responses.

The anatomy of a request

A request has exactly four parts.

  1. A method. The verb. GET = "give me something, change nothing." POST = "here is data, create something." PATCH = "modify part of something." DELETE is self-explanatory.
  2. A URL. Uniform Resource Locator, the address. https://erp.example.com/api/orders?status=open breaks into scheme, host, path (/api/orders) and query string (?status=open).
  3. Headers. Labeled notes on the envelope: what format the body is in, who you are, what you accept back.
  4. A body. Optional, usually JSON. GET normally has none, POST usually does.

The anatomy of a response

A response has three parts: a status code (a three-digit number summarizing what happened), headers, and a body. The status codes group neatly:

  • 2xx: it worked. 200 OK, 201 Created.
  • 3xx: go look somewhere else. 301 Moved Permanently.
  • 4xx: you did something wrong. 400 Bad Request, 401 Unauthorized (who are you?), 403 Forbidden (I know who you are, and no), 404 Not Found, 409 Conflict (you tried to sell 200 shirts and there are 40).
  • 5xx: I did something wrong. 500 Internal Server Error means your code crashed.

A whole round trip with curl

curl sends an HTTP request from the terminal and prints exactly what comes back. No browser, no guessing. Here is one full round trip against a running dev server.

$ curl -i -X POST http://localhost:3000/api/orders \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer sk_test_2f9c4a17b0" \
    -d '{"customerId":"cus_1042","sku":"SS26-TEE-01-M","quantity":24}'

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Sat, 25 Jul 2026 16:04:11 GMT
Content-Length: 66

{"orderId":"ord_88f31","status":"pending","reserved":24,"ats":176}

Walkthrough, piece by piece.

  • -i tells curl to include the response headers, not just the body. -X POST sets the method, because we are creating something. The backslashes at line ends mean "this command continues on the next line."
  • http://localhost:3000/api/orders is the URL. localhost means "this same computer," and :3000 is the port — a numbered door, because one machine can run many servers at once. So: knock on door 3000 of my own laptop and ask for /api/orders.
  • -H "Content-Type: application/json" is a header announcing "the body I'm sending is JSON." Without it the server may not know how to read your data.
  • -H "Authorization: Bearer sk_test_..." proves who you are. It is a secret string the server issued earlier, and "Bearer" means "whoever holds this gets these permissions."
  • -d '{...}' is the body: the order we want to create, as JSON.

Now the response. HTTP/1.1 201 Created gives the protocol version, then the status, and it is 201 rather than 200 because a new thing came into existence.

The headers say the body is JSON as UTF-8, when it was generated, and that it is 66 bytes long, which is exactly the length of the line underneath. Then comes a blank line (the formal separator between headers and body) and the JSON itself: an order id, its status, the 24 units we reserved, and the ATS that remains. It was 200 before the call, we took 24, so 176 are left.

What happened between the request and the response

In between, in order:

  1. curl opened a connection to port 3000 and sent the request text: method, path, headers, blank line, body.
  2. Your Node process, waiting in a loop for connections, woke up and read it. Next.js (the framework we build on, meaning a ready-made application skeleton that handles routing and server plumbing so you only write what is specific to your app) matched path /api/orders and method POST to one function in one file, and called it.
  3. That function read the Authorization header and confirmed the caller may create orders. (If not: stop, respond 401.)
  4. It parsed the JSON body into a real object and validated it: positive whole quantity? real SKU? (If not: stop, respond 400.)
  5. It opened a transaction against Postgres, checked available-to-sell, found 200, confirmed 24 ≤ 200, wrote an order row and a reservation row, committed. (If ATS had been 12: stop, respond 409 Conflict.)
  6. It returned the result as JSON with status 201. Node wrote that down the connection and closed it, and curl printed it.

Every button in every app you have ever used is that. A browser does exactly what your curl did, then redraws part of the screen using the response. That is the whole trick.

Core principle

Treat the server as the only place truth lives. Anything the browser sends can be forged. A user can open developer tools and change any value, or skip your interface entirely and use curl. So every rule that matters (can this person see this? is this in stock? is this price correct?) must be enforced on the server. A check in the browser gives honest users a fast, friendly message, but it stops nobody who wants past it.

What a database is, and why files won't do

A database is a program whose entire job is storing data safely and answering questions about it quickly. You never open its files. You ask it, in a language called SQL (Structured Query Language), and it answers. Many people say "sequel," others spell out the three letters. Ours is PostgreSQL, usually shortened to Postgres.

Data lives in tables: a grid of named columns defining what each field is, and rows, one per thing. A styles table has one row per T-shirt design. That is spreadsheet-shaped, so beginners reasonably ask why not just use a spreadsheet, or write to a file. The reasons are concrete.

  • Two things happen at once. One shirt left, and two reps click "sell" in the same second. With files both read "1 available," both write "0," and you sold the same shirt twice. A database has transactions: groups of operations that either all happen or none do, with other work held back until they finish. This alone justifies the category.
  • The power goes out mid-write. A half-written file is corrupt garbage. A database writes changes to a durable log before touching the data, so an interrupted transaction is cleanly undone on restart.
  • Finding things. Searching a file means reading all of it. A database keeps indexes — sorted lookup structures, like the index at the back of a book — so "open orders for customer 1042" stays fast at a hundred rows or ten million.
  • Rules that cannot be broken. Declare that quantity must be positive, that order lines point at real SKUs, that style codes are unique. The database then refuses anything violating them, whichever code asked. A spreadsheet lets anyone type "twelve-ish" into a number column.
  • Multi-tenancy. One installation of our system serves several separate brands (each one a tenant), and Brand A must never see Brand B's orders. Postgres can enforce that itself with row-level security, a feature where the database attaches a filter to every query against a table, rather than you hoping that every query somebody writes remembered to filter. Chapter 5 covers it in full.

Meeting Postgres at the psql prompt

You can touch one in about a minute. psql is the terminal client for Postgres.

$ psql "postgresql://postgres:hunter2@localhost:5432/erp"
psql (18.4)
Type "help" for help.

erp=#

Walkthrough. That long string is a connection string, packing five things into one line: the kind of database, the username, the password, the host and port (5432 is Postgres's traditional door number), and the database name.

Your application connects with exactly the same string, stored as an environment variable. The erp=# prompt means you are in and it is waiting for SQL. Version 18 is the current major release as of July 2026, and anything from 15 upwards behaves identically for everything in this chapter.

CREATE TABLE styles (
  id          bigserial PRIMARY KEY,
  style_code  text      NOT NULL UNIQUE,
  name        text      NOT NULL,
  created_at  timestamptz NOT NULL DEFAULT now()
);

INSERT INTO styles (style_code, name)
VALUES ('SS26-TEE-01', 'Boxy Pocket Tee');

SELECT id, style_code, name FROM styles;
erp=# CREATE TABLE styles (...);
CREATE TABLE
erp=# INSERT INTO styles (style_code, name)
erp-#   VALUES ('SS26-TEE-01', 'Boxy Pocket Tee');
INSERT 0 1
erp=# SELECT id, style_code, name FROM styles;
 id |  style_code  |      name
----+--------------+-----------------
  1 | SS26-TEE-01  | Boxy Pocket Tee
(1 row)

erp=# INSERT INTO styles (style_code, name)
erp-#   VALUES ('SS26-TEE-01', 'Duplicate!');
ERROR:  duplicate key value violates unique constraint
"styles_style_code_key"
DETAIL:  Key (style_code)=(SS26-TEE-01) already exists.
erp=# \q

Walkthrough. CREATE TABLE defines the shape. Each column gets a name and a type: text for words, bigserial for a whole number the database fills in for you, counting up, timestamptz for a moment in time with its timezone.

PRIMARY KEY means "uniquely identifies a row," NOT NULL means "never empty," UNIQUE means "no two rows share this value," DEFAULT now() fills the timestamp in for you. (bigserial is Postgres's own shorthand. The SQL-standard spelling of the same idea is bigint generated always as identity, and you will meet both in real projects.)

INSERT adds a row (INSERT 0 1 means "one row inserted") and SELECT asks a question and gets a grid back. The second command spans two lines: erp=# is the prompt for a new statement and erp-# means "still finishing the previous one," because psql waits for the semicolon.

Then the important bit: the second insert was rejected. We told the database style codes are unique and it enforced that by refusing. No application bug, no rushed intern, no direct database poke can create a duplicate style code ever again. \q quits psql (backslash commands are psql's own, not SQL).

Push correctness as far down as it will go

Carry that principle out of this chapter. A rule in the database holds for every path into your data — your web app, a script, a colleague in psql at midnight. A rule in your form holds for one path, until someone adds a second.

Version control: git, and why your laptop is not the product

Version control records the complete history of your project: every change, when, and why. Git is the one everybody uses. GitHub is a website that hosts git repositories — a different thing, easily confused.

Git buys you three things:

  • undo at any depth;
  • a record of why (every change carries a message, so "why does this subtract 3?" is answerable in six months);
  • and safe experiments.

Two nouns to nail down. A commit is a saved snapshot of the entire project at one moment, with a message and an author. It captures the whole tree, and it has a permanent id you can return to years later. You make them constantly.

A branch is a movable name pointing at a commit. The default is main, by convention "the version that works."

Before you start something risky, you make a branch. You work there and main stays untouched. If it works out you merge it in, and if not you delete the branch and nothing was harmed. A branch is cheap — literally a small file containing a commit id.

The five commands a solo developer needs

Five commands cover almost everything you will do working alone. Here they are in the order you use them.

$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   apps/web/src/lib/inventory.ts

no changes added to commit (use "git add" and/or "git commit -a")

Walkthrough. git status is command number one. Run it constantly: before committing, after any confusing moment, whenever you're unsure.

It tells you which branch you're on, whether you match the copy on the server (origin is git's default name for that server copy, so origin/main means "the main branch as GitHub last saw it"), and exactly which files you changed. Here: one modified file, not yet marked for saving. Git even prints the commands you probably want next.

$ git add apps/web/src/lib/inventory.ts
$ git commit -m "fix: subtract reserved units when computing ATS"
[main 9c1f0ab] fix: subtract reserved units when computing ATS
 1 file changed, 4 insertions(+), 2 deletions(-)

$ git push
Enumerating objects: 13, done.
Counting objects: 100% (13/13), done.
Writing objects: 100% (7/7), 812 bytes | 812.00 KiB/s, done.
To github.com:you/erp.git
   3e7a112..9c1f0ab  main -> main

$ git log --oneline -3
9c1f0ab fix: subtract reserved units when computing ATS
3e7a112 feat: ATS endpoint returns per-size availability
b4d9e60 chore: add zod for request validation

Walkthrough. git add stages a file ("include this in the next snapshot") so you can commit some changes and not others. git commit -m "..." takes the snapshot, and git replies with the branch, the new commit's short id, and a summary. git push uploads your commits to GitHub so they exist on more than one hard drive. git log --oneline shows recent history newest first — a readable diary.

The fifth command is git checkout -b some-branch, which creates a branch and moves you onto it. (Recent versions of git spell the same thing git switch -c some-branch, and both work.) Use it before anything you might regret. That is the whole solo kit: status, add, commit, push, checkout -b.

Why your laptop is not the product

The code on your machine and the code your customers use are two different things that happen to look alike.

Your laptop holds a half-finished experiment, a test database with three fake orders, a debug line you left in, and a config file pointing at localhost. It is a workshop, and it is supposed to be messy.

What customers run is a copy of one specific commit, on a server that never sleeps, pointed at the real database with real money in it. It changes only when you deliberately push to it. Saving a file on your laptop does nothing.

That gap is a feature: it is the only reason you can experiment freely.

Deploying, production, and environments

Deploying means taking a specific commit and making it the version that runs on a real server for real users. Mechanically:

  1. fetch the code,
  2. install dependencies,
  3. compile,
  4. start the new process,
  5. send traffic to it,
  6. and retire the old one.

Hosting platforms do all of that for you when you push to main. This book uses Vercel, the company behind Next.js.

Production is the environment real users touch. "In prod" means "live, with real consequences." You will run three environments.

LocalStagingProduction
Runs onYour laptopA real serverA real server
URLhttp://localhost:3000staging.erp.example.comerp.example.com
DatabaseLocal or dev PostgresSeparate copy, fake dataThe real one
Data isDisposableDisposableIrreplaceable
Who uses itYouYou + client reviewCustomers
Breaking it costsNothingAn apologyMoney and trust
Updated bySaving a filePushing a branchMerging to main

Staging is production-shaped but harmless: same code path, same deployment process, same kind of database, with data nobody cares about. It is where you find the mistakes that only appear on a real server.

Environment variables and secrets

Your code needs to know things that differ per environment: which database to talk to, which API keys to use. None of that belongs in your source code. The values change from one environment to the next, some of them are secrets, and source code gets copied, shared and backed up.

An environment variable is a named value handed to a process when it starts, from outside the code. The program reads the name, and the value comes from wherever it is running. Same code, different configuration.

$ cat apps/web/.env.local
DATABASE_URL=postgresql://postgres:hunter2@localhost:5432/erp
NEXT_PUBLIC_SUPABASE_URL=https://abcdefgh.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

$ grep -n "env.local" .gitignore
5:.env.local
const connectionString = process.env.DATABASE_URL;

if (!connectionString) {
  throw new Error("DATABASE_URL is not set — check your .env.local file");
}

Walkthrough. The .env.local file holds one NAME=value pair per line and is listed in .gitignore, so it never enters version control. (Supabase, which appears twice here, is a hosting company that runs a Postgres database for you and adds logins and file storage on top. Chapter 10 covers it.)

In production the hosting platform holds the same names with different values and injects them at start-up. In code, process.env.NAME reads one.

Notice the guard: if the variable is missing we crash immediately with a clear message. A missing database URL should fail loudly at start-up, not mysteriously three screens deep.

One naming rule will bite you: in Next.js a variable prefixed NEXT_PUBLIC_ is copied into the JavaScript sent to the browser when the app is built, where anyone can read it. Everything else stays on the server.

NEXT_PUBLIC_SUPABASE_URL is fine, because an address is not a secret. Putting that prefix on a service role key would publish the keys to your kingdom on every page load. Two consequences follow from "when the app is built": the value is frozen at build time, so changing it later means rebuilding, and you cannot un-publish one by deleting the variable afterwards.

Secrets in git are forever

If you commit a password or an API key and push it, treat it as permanently compromised. Deleting it in a later commit does not remove it from history. GitHub itself scans public repositories for known credential formats and notifies the issuing provider so the key can be revoked, and less friendly automated crawlers are looking too. The only correct response is to rotate the credential: revoke the old key at the service, issue a new one. Do that before you fix the file.

Reading errors and debugging

You will spend more time understanding broken things than writing new ones. That is what the work is. The people who look fast at it are working from a method, and the method is learnable.

Read the error. Actually read it.

The universal beginner move is to see red text, panic, and scroll past it. The error usually contains the answer. Here is one:

$ npm run dev

TypeError: Cannot read properties of undefined (reading 'quantity')
    at reserveInventory (/Users/you/erp/apps/web/src/lib/inventory.ts:41:9)
    at POST (/Users/you/erp/apps/web/src/app/api/orders/route.ts:27:18)
    at async /Users/you/erp/node_modules/next/dist/server.js:82:9

Walkthrough. Take it in three pieces. The type is TypeError: something was the wrong kind of thing.

The message: "Cannot read properties of undefined (reading 'quantity')". Read it literally: the code tried to get .quantity from something, and that something was undefined — JavaScript's word for "there is nothing here." The quantity field is innocent. What went missing is the container it lives in: an order line, a database row, something you expected to find and did not.

The stack trace: those indented at lines are the chain of function calls that led to the explosion, innermost first. Read it as a story in reverse: something inside Next.js called POST, which called reserveInventory, which blew up at inventory.ts, line 41, column 9. Go to that exact line first.

Notice the last frame sits inside node_modules, which holds library code you did not write. Nearly always, the bug is in the topmost frame with your file path in it.

The four-step method

1. Reproduce it reliably. Find the exact steps that make it happen every time. A bug you can trigger on demand is nearly solved. One that "sometimes happens" will eat your week. If it seems random, look for the hidden variable: time, ordering, an empty list, a second user.

2. Print the state. Do not reason about what the values must be. Look.

export async function reserveInventory(sku: string, quantity: number) {
  const row = await db.getInventoryRow(sku);

  console.log("[reserveInventory] sku=", sku, "row=", row);

  return row.onHand - row.reserved >= quantity;
}
[reserveInventory] sku= SS26-TEE-01-M  row= undefined

Walkthrough. One console.log — the instruction that prints a value to the terminal — placed just before the crash, showing both the input and the thing we are about to use.

The output settles it: the SKU looks perfectly correct and the database returned nothing for it. The question changes from "why is my code broken" to "why does the database have no row for SS26-TEE-01-M?" That one is answerable in psql in ten seconds. Label your logs so you can find them, and delete them when you're done.

3. Bisect. If you cannot see the cause, cut the problem in half. Does the data look right leaving the database? Then look downstream. Does the API return the right JSON under curl? Then the server is fine and the bug is in the browser. Each check eliminates half the system, and six halvings take you from "somewhere in 10,000 lines" to "these 150 lines."

The same works over time: use git log to find when it last worked, then check out commits in between until you find the one that broke it. Git automates that search with git bisect.

4. Ask what you assumed. Every bug is a false belief. Say your assumptions out loud, then verify each instead of trusting it.

"I assumed the seed script ran," the seed script being the small program that fills a fresh database with starter rows. "I assumed the SKU in the form matches the one in the table." "I assumed the server restarted after I saved." That last one accounts for an embarrassing share of all debugging time, mine included.

And if you have been stuck more than forty-five minutes: stop, stand up, then write down in plain English exactly what you expected and exactly what happened. Most people solve the bug while writing the second sentence.

How to use this book

Thirty-one chapters follow this one, arranged in five parts. They are ordered as a build: most chapters produce something that works, and later chapters assume it exists.

  • Part 0: Orientation (chapters A–C). This chapter, then B: What an ERP Actually Is (the business the software has to model, and the exact vocabulary your users speak), and C: The Architecture Map, which puts every moving piece of the system on one page.
  • Part 1: The Business of Fashion Wholesale (chapters D–M). Ten chapters with very little code: the wholesale business model, merchandising and line planning, costing and margin, sourcing and production, sales channels and the selling season, retail partner compliance and chargebacks, warehouse and logistics, finance and cash, metrics and planning, and the legal and sustainability rules. Each one ends by translating that reality into tables, fields and rules. If you are impatient to build, read D, F and I at minimum before you design any schema.
  • Part 2: Core Engineering (chapters 0–10). Data structures and algorithms as an ERP actually uses them, ledger-first data modeling, Postgres in depth, concurrency and correctness (concurrency being two things happening at the same time, like two reps selling the last shirt in the same second), integration engineering, multi-tenancy and security, offline-first sync, import and migration tooling, reporting and computed state, testing and operations, and the specifics of our stack: Next.js, Supabase and Turborepo.
  • Part 3: Delivery and Reference (chapters N, 11, 12). Getting the system adopted by the people who have to use it every day, the complete reference schema, and the roadmap, decision log and glossary you will keep open in another tab.
  • Part 4: Running It in Production (chapters O–S). Hosting, domains and environments; logins and access; running your own server versus paying a cloud provider; the trade show playbook for the week the system has to work on a show floor with bad wifi; and a runbook of the failures you will actually meet, with the fix for each.

Which chapters to read first

Two chapters carry the most weight for everything after them: chapter 1, which fixes the shape of your data, and chapter 2, which teaches the database that stores it. Read those in order and do not skip them, because chapter 8's reporting work assumes chapter 1's ledger exists, and chapter 6's offline sync assumes almost everything.

Expect a few evenings per chapter if you type everything and stop to understand it, longer for the engineering chapters in Part 2. If one takes three times as long as you hoped, that is normal and not a verdict on you.

What "good enough for now" looks like

Good enough means it does the thing correctly for the real cases, it fails loudly rather than quietly, and you could explain to a colleague why it works. It does not have to be pretty, fast or general. Ugly code that computes ATS correctly beats clever code off by three. When you catch yourself building a flexible abstraction for a requirement nobody asked for, ship the direct version and move on.

Commit constantly, and type the code out

Two habits to adopt today. Commit constantly, every time something works, even slightly. A commit costs three seconds and buys an undo button. And type the code out rather than copying it: typing produces typos, typos produce errors, and reading errors is how you learn to read errors.

Last thing. You will feel lost regularly, as does everyone building real systems, including people twenty years in. What separates them is a method for being lost: read the error, print the state, cut the problem in half, question the assumption. You have that method now. Turn the page.

Field notes & further reading

Exercise

Exercise 1: Make your machine a workshop.

  1. Open your terminal. On macOS press Cmd-Space, type Terminal, Enter. On Windows, install WSL (Windows Subsystem for Linux) and open Ubuntu.
  2. Install Node.js, taking the version labeled LTS on the download page, then confirm with node --version and npm --version. As of July 2026 that is Node 24 or 22, and anything from 22 upwards works for this book. Install a code editor too: Visual Studio Code is free and the usual choice.
  3. Confirm git with git --version, then set your identity with git config --global user.name "Your Name" and the matching user.email.
  4. Create a project under version control: mkdir -p ~/erp-warmup && cd ~/erp-warmup && git init. Add a notes.md saying in one sentence what an ERP is, then git add notes.md, git commit -m "first commit", git log --oneline.
  5. Practice navigating: pwd, ls -la, cd .., pwd again, then cd - to jump back. Do it until it feels boring.

Exercise 2: Say hello to a real Postgres database.

  1. Create a Supabase project on their free plan and copy the Postgres connection string from its dashboard. (As of July 2026 the free plan allows two active projects, and pauses a project after a week with no activity. If yours is asleep later, wake it from the dashboard.)
  2. Install the Postgres client tools so you have psql, then connect with psql "<your-connection-string>". You should land at a postgres=> prompt.
  3. In psql, create the table and row from earlier in this chapter and SELECT them back. Then insert a duplicate style_code on purpose and read the error. Quit with \q.
  4. Back in ~/erp-warmup, run npm init -y then npm install postgres. Open package.json and run ls node_modules | wc -l.
  5. Create .env.local holding DATABASE_URL=<your-connection-string>, and .gitignore holding node_modules and .env.local. Run git status and confirm neither is listed.
  6. Write hello.mjs that reads process.env.DATABASE_URL, connects, runs select style_code, name from styles, prints the rows and closes the connection. Run it with node --env-file=.env.local hello.mjs.

When you are done you should have a terminal you can navigate without thinking, a git repository with at least two of your own commits, a real Postgres database in the cloud holding one style you created by hand, and a program on your laptop that connects to it over the internet and prints its contents. That last one (your code, someone else's server, real data coming back) is the shape of everything in the rest of this book.