By the end of this tutorial you'll have a Git repo, a TypeScript HTTP server that says "hello", a working linter, a one-page design doc, and the habits that make every later module easier.
← Back to Module 01 overviewnvm / fnm / volta.git --version should print something.gh CLI installed and authenticated, or just be ready to create a repo via the web UI.url-shortener with main as the default branch.http://localhost:3000.npm run lint and npm run dev both work.docs/design.md committed to the repo.main requiring PRs.From an empty parent folder:
mkdir url-shortener && cd url-shortener git init -b main gh repo create url-shortener --public --source=. --remote=origin # Or skip the gh command and create the repo via github.com later.
git status shows "On branch main".npm init -y npm pkg set type="module" npm pkg set engines.node=">=20"
package.json exists and contains "type": "module".npm install express
npm install -D typescript tsx @types/node @types/express \
eslint @eslint/js typescript-eslint prettier
npm install hangs on a corporate network, set npm config set registry https://registry.npmjs.org/ and try again.Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
Create src/server.ts:
import express from 'express';
const app = express();
const port = Number(process.env.PORT ?? 3000);
app.get('/health', (_req, res) => res.json({ ok: true }));
app.get('/', (_req, res) => res.send('URL Shortener — hello!'));
app.listen(port, () => console.log(`listening on :${port}`));
npm pkg set scripts.dev="tsx watch src/server.ts" npm pkg set scripts.build="tsc" npm pkg set scripts.start="node dist/server.js" npm pkg set scripts.lint="eslint src" npm pkg set scripts.format="prettier --write ."
Run it:
npm run dev
http://localhost:3000/health in a browser. You should see {"ok":true}.Create eslint.config.js:
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{ ignores: ['dist/**', 'node_modules/**'] }
];
Create .prettierrc.json:
{ "singleQuote": true, "semi": true, "printWidth": 100 }
npm run lint exits with code 0..editorconfig:
root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true
.gitignore:
node_modules dist .env .env.* !.env.example *.log .DS_Store
Create docs/design.md with this skeleton — keep it to one page.
# URL Shortener — Design
## Problem
Users want short, shareable URLs that redirect to long ones.
## Goals
- POST /shorten → returns a short code
- GET /:code → 302 redirects to the original URL
- Per-user ownership of links
- 99.95% redirect availability over 30 days
## Non-goals (v1)
- Custom domains
- Click analytics dashboard (will arrive in module 6)
- Mobile app
## Sketch
Client → Express → Redis (cache) → Postgres
↘ Queue → Worker → clicks table
## Open questions
- ID scheme: random base62 vs hash? (decide in module 2)
- Anonymous quota? (decide in module 5)
git add . git commit -m "module 01: scaffold typescript express server" git push -u origin main
On GitHub: Settings → Branches → Add rule for main:
main.git push origin main from a new commit on main directly — it should be rejected.npm install after creating package.json.npx tsx --version works; if not, reinstall: npm i -D tsx.PORT=3001 npm run dev (or kill the other process).process — make sure @types/node is installed.You've got a clean slate. Next up: actually doing something useful with it — designing the API and shaping the database.