For ten modules you have had a guided example: Nómada Tasks was built in front of you, lesson by lesson, and every new concept arrived with its slice of code already worked out. That ends here. In this module you build your own product, on your own, with the same method and with nobody handing you the code. Nómada Tasks does not disappear: it becomes the reference — six tasks, 48 hours, rules R1–R10, 124 tests, 58.3 kB in three requests, an LCP of 1.9 s — against which you will compare whatever you build. And this first lesson deliberately does not write a single line of business logic, because the most expensive mistake in a project of your own is not an unexpected undefined: it is starting to code without knowing what you are building. Here you will learn to turn an idea into a defensible plan: choose the product, trim it down to an honest MVP and write down in black and white what you are not going to do; write user stories with acceptance criteria and prioritize them with MoSCoW; design the data model and the business rules — the new R11 to R15 — before touching the editor; decide the layered architecture and the boundaries that are never crossed; set up the repository, the toolchain and the Git workflow; plan in milestones with a Gantt chart and an estimate you do not lie to yourself about; and lock in, from minute zero, the Definition of Done and the performance and accessibility budget. By the end you will have three real deliverables: the scope document, the data model with its rules, and an initialized repository that is green.
Contents
- What changes from now on
- The default product: Orbita
- Three alternative domains with the same method
- Setting the scope: the MVP
- The list of what will NOT be done
- User stories with acceptance criteria
- Prioritizing with MoSCoW
- The data model, written before you code
- The new business rules: R11 to R15
- The layered architecture and the boundaries that are never crossed
- The environment: repository and toolchain
- The starting folder structure
- Git for real: branches, commits and reviews
- Milestones and honest estimation
- The Definition of Done
- The performance and accessibility budget
- The
README.mdtemplate - Common Mistakes and Tips
- Exercises
- Conclusion
- What changes from now on
Up to lesson 10-06, the course worked like this: a concept was explained, it was applied to Nómada Tasks, and the code appeared. You read it, you understood it, you adapted it in the exercises. That is the right way to learn the fundamentals, and it is exactly what had to happen.
But there is an enormous difference between understanding somebody else's code and producing your own code from scratch, and you do not close that gap by reading more. You close it by building. This module is that build.
From here on, three things change:
| Before (M1–M10) | Now (M11) |
|---|---|
| The code was handed to you solved | You write the code; here you get method, templates and criteria |
| The exercises were practice | The exercises are project deliverables |
| The solutions were the correct code | The solutions are acceptance criteria and rubrics |
| The project was Nómada Tasks | The project is yours; Nómada Tasks is the reference |
| The goal was to learn a concept | The goal is to finish a product |
That last point deserves emphasis. The skill that separates someone who knows JavaScript from someone who is a developer is not knowing more array methods: it is finishing things. Finishing means it works, it is tested, it can be deployed, another person understands it, and you can defend why it is built the way it is. That is the bar for this module.
And there is an honest warning worth reading slowly. The hard part of a project of your own is not technical. It is that nobody tells you when you are done, nobody corrects your scope when it grows, and nobody warns you that you have spent three days polishing an animation while the core functionality is still half-built. The method in this lesson exists precisely to put up those guardrails before you need them.
- The default product: Orbita
The default product for this module is called Orbita. It is a work-management application for small teams: the same family as Nómada Tasks, but clearly more ambitious, with six extensions that were never implemented in the course and that you will have to solve on your own.
A note about data. Orbita, its sample users and any organization that appears in your test data must be fictional. Do not use real names, email addresses or data belonging to colleagues, clients or family members, not even "just to test": the moment that reaches a public repository or a deployment, it stops being a test and becomes processing of personal data. In lessons 11-03 and 11-05 we will come back to this point in detail.
The six extensions on top of Nómada Tasks are these:
| # | Extension | What it adds over Nómada Tasks | Course concept it reuses |
|---|---|---|---|
| E1 | Users and assignment | People stop being a string and become entities with id, role and permissions; assignee and reviewer are references |
04-01, 05-02, 05-03 |
| E2 | Subtasks | A task can be broken down into a tree up to three levels deep, with aggregated hours and progress | 03-07 (recursion) |
| E3 | Tags with multiple filtering | Filter by several tags at once, with AND / OR modes, combinable with the other filters | 03-06, 04-05 |
| E4 | Calendar view | A monthly grid that places tasks by dueDate, with month-to-month navigation |
04-04, 06-05, Intl from 07-06 |
| E5 | Exportable workload report | Workload per person and per week, exportable to CSV and JSON downloadable from the browser | 04-05 (reduce), 07-06 (Blob) |
| E6 | Change history | An immutable log of who changed what and when, with an audit view per task | 04-08, 05-03 |
None of the six is decorative. Each one forces you to solve a real problem that Nómada Tasks sidestepped:
- E1 breaks the
assignee: 'Iván'simplification. As soon as there are referenced entities, referential integrity appears (what happens to the tasks of a user who is deleted?) along with permissions. - E2 turns a flat list into a tree. Everything that was
array.filter(...)now needs recursive traversal, and total hours stop being a straight sum. - E3 looks trivial and is not: combining N filters with two different semantics and keeping them in the URL (06-04, router) forces you to think about state.
- E4 is the first view that is not a list, and therefore the first one you cannot solve by copying
BoardView. - E5 is the first feature that produces a file, with everything that implies: format, encoding, filenames, and the fact that a badly escaped CSV breaks the moment a title contains a comma.
- E6 introduces immutable, ever-growing data, which is exactly the kind of data that blows up
localStorage(lesson 11-03).
Orbita in one sentence, which is how you should be able to describe any product:
Orbita is a web application that lets a small team plan its work in tasks and subtasks, see who is overloaded and when things are due, and know at all times who changed what.
If you cannot write that sentence for your product, you do not yet know what you are building.
- Three alternative domains with the same method
If the task domain bores you — a perfectly legitimate reason; you are going to spend weeks with this — pick another one. The method is identical and the whole module works the same way. These are the three proposed alternatives, each with the six extensions mapped onto it:
| Extension | Orbita (tasks) | Capacity (room bookings) | Streak (habits) | Shelf (inventory) |
|---|---|---|---|---|
| E1 · Users | Assignee and reviewer | Who books and who authorizes | Person and support group | Warehouse owner |
| E2 · Tree | Subtasks | Room → subspaces (desk, booth) | Habit → daily steps | Category → subcategory → item |
| E3 · Multiple filter | Tags | Equipment (projector, whiteboard) | Areas (health, study) | Product tags |
| E4 · Calendar | Due date | The central view: occupancy by time slot | Monthly consistency grid | Expiry dates and restocking |
| E5 · Report | Workload per person | Occupancy per room and week | Streak and completion percentage | Stock valuation |
| E6 · History | Who changed what | Booking changes and cancellations | Daily log (it already is a history) | Stock in and out movements |
| Characteristic hard rule | No closing with open subtasks | No two overlapping bookings in the same room | One entry per habit per day | Stock can never go negative |
Look at the last row: every domain has one characteristic hard rule that gives the model its personality and that will need the most tests. In Capacity it is interval overlap, a classic problem and considerably subtler than it looks. In Streak it is uniqueness per day, which forces you to think about time zones. In Shelf it is a numeric invariant you have to defend in every operation.
Criteria for choosing, in order of importance:
- That you care about it. You are going to spend many hours on it and nobody is going to force you. Motivation is as real a project resource as time.
- That you can show it to someone. If in an interview you can explain the domain in thirty seconds, it works.
- That it has at least one non-trivial hard rule. A CRUD with no rules proves nothing; it is the project everybody does.
- That it fits. If your idea is "like Jira but better", it does not fit. Trim it until it fits, which is exactly the next section.
Throughout the rest of the module I will use Orbita in every example, but each template is directly reusable with any of the other three domains. When you see a table or a template, swap the names and carry on.
- Setting the scope: the MVP
MVP stands for Minimum Viable Product. Both words matter and one of the two is almost always misunderstood.
- Minimum does not mean "shoddy". It means there is nothing you can remove without it ceasing to solve the problem.
- Viable does not mean "a demo". It means somebody could genuinely use it for what it promises.
An Orbita MVP that did not let you mark a task as done would be minimum but not viable. One with color themes, customizable keyboard shortcuts and email notifications would be viable but not minimum.
The test I use to decide whether something belongs in the MVP is a single question:
If this were not here, would the product still do what its sentence says?
If the answer is "yes, awkwardly, but it works", it is not MVP. It goes into version 1.1.
Applied to Orbita, this is the cut:
| Feature | MVP? | Reason |
|---|---|---|
| Create, edit and delete tasks | Yes | Without this there is no product |
| Change status through the valid transitions (R6) | Yes | It is the entire workflow |
| Assign an assignee (E1) | Yes | "Who does what" is in the product sentence |
| One level of subtasks (E2 partial) | Yes | Decomposition is the differentiator; three levels can wait |
| Filter by assignee, status and tags (E3) | Yes | Without a filter, a board with 60 tasks is useless |
| Local persistence | Yes | A manager that loses your data on reload is not viable |
| Change history (E6) | Yes | It is an explicit promise of the product sentence |
| Workload report (E5) | Yes, minimal | The on-screen table yes; the CSV export no |
| Calendar view (E4) | No | Useful, but the list sorted by date covers the need |
| Export to CSV/JSON | No | Convenience, not necessity |
| Three levels of subtasks | No | One level demonstrates the tree; three only add edge cases |
| Synchronization with an API | No | The MVP is single-device; 11-03 extends it |
| Multi-user real time | No | Very high cost, low value for a three-person team |
| Full roles and permissions | No | The MVP assumes all users are trusted |
| Dark mode, themes, animations | No | Cosmetic |
Out of 15 candidates, 8 make it in. That ratio (roughly half) is normal and healthy. If your MVP contains 90 % of everything you thought of, you have not trimmed: you have written a wish list.
A practical, road-tested size rule: a personal-project MVP should be something you can build in 6 to 10 weeks putting in 6 to 10 hours a week. If your honest estimate (section 14) comes out higher, trim now, because your estimate is optimistic, not pessimistic.
- The list of what will NOT be done
This is the section of the scope document most people skip and the one that saves the most projects. An MVP defined only by what it includes is ambiguous: everything not mentioned sits in a gray zone where, on a Tuesday afternoon, adding "just one little thing" will seem reasonable.
Explicitly writing down what will not be done turns every later addition into a conscious decision instead of a drift.
The template has three columns and lives in docs/scope.md:
## Out of scope (v1.0)
| What will not be done | Why | When it gets reconsidered |
|---|---|---|
| Calendar view | The date-sorted list covers the main need | v1.1, after milestone H5 |
| CSV/JSON export | Convenience; no acceptance criterion depends on it | v1.1 |
| Subtasks deeper than one level | One level already demonstrates the tree; more levels only add edge cases | v1.2, if a real user asks for it |
| Server synchronization | Requires a backend; the MVP is single-device | v2.0 (lesson 11-03 prepares the boundary) |
| Real time / collaboration | Disproportionate cost for three users | Not planned |
| User authentication | There is no server; client-only authentication is security theater | v2.0, alongside the backend |
| Email notifications | Requires a server and an email provider | Not planned |
| Internationalization into several languages | A single language; formats will use `Intl` | v1.2 |
| Native mobile app | The PWA covers the mobile use case | Not planned |
| Themes and visual customization | Cosmetic; affects no acceptance criterion | Not planned |Three details of the template that are not decoration:
- The "Why" column protects you from yourself. Three weeks from now you will not remember the reasoning, only the temptation.
- "When it gets reconsidered" avoids the resentful "never". Deferring is not rejecting, and saying it that way makes the cut far easier to accept.
- "Not planned" is a legitimate answer and you should use it without guilt. Not everything deferred has to come back.
A specific note about the authentication line, because it is a classic mistake in portfolio projects: a login screen without a server protects nothing. If the data lives in localStorage, anyone with developer tools can see it. Putting a password form in front of it is worse than not putting one, because it communicates a security that does not exist. If your project genuinely needs authentication, it needs a server, and that is version 2.0 (lesson 11-07).
- User stories with acceptance criteria
A user story describes a need from the point of view of the person who has it, not the person implementing it. Its value is not the pretty format: it is that it forces you to say for whom and what for, and those two words often decide the technical design.
The classic template:
And what really makes a story useful are its acceptance criteria: verifiable conditions that let you say "done" without argument. They are written in Given / When / Then form, which has an enormous and entirely deliberate advantage: it translates almost literally into a test of the kind you learned in 08-03 and 08-06.
Here are three real Orbita stories, in full.
6.1 Story H-04: breaking a task down
### H-04 · Break a task down into subtasks
**As a** team coordinator
**I want** to split a large task into subtasks
**so that** I can spread the work across several people and see partial progress
**Acceptance criteria**
1. **Given** an existing task in `pending` or `in-progress` status,
**when** I add a subtask with a title and hours,
**then** the subtask appears nested under its parent task
**and** the parent's hours become the sum of its subtasks' hours.
2. **Given** a task with two subtasks, one `done` and one `pending`,
**when** I try to mark the parent as `done`,
**then** the application blocks it with a message that names
the open subtask (rule R13).
3. **Given** a task with subtasks,
**when** I look at its progress,
**then** I see the percentage of finished subtasks
**and** that percentage is announced with `aria-live` when it changes.
4. **Given** a subtask,
**when** I try to make it the parent of its own parent task,
**then** the operation is rejected with `RuleError` (no cycles, R12).
**Technical notes**: tree traversal is recursive (03-07).
Maximum depth is 3; the MVP only exposes 1 level.Notice criterion 2. It does not say "you cannot do it": it says exactly what happens, with which message, citing the rule. That precision is what turns the criterion into a five-line Jest test.
And notice criterion 3: accessibility is inside the acceptance criterion, not in a separate list reviewed at the end. That is the only way it does not get forgotten. I will come back to this in section 16 and in lesson 11-02.
6.2 Story H-07: filtering by several tags
### H-07 · Filter by several tags at once
**As a** team member
**I want** to filter the board by several combined tags
**so that** I can quickly find the work in a specific area
**Acceptance criteria**
1. **Given** a board with tagged tasks,
**when** I select two tags in *any* mode (OR),
**then** I see the tasks carrying at least one of the two.
2. **Given** the same selection in *all* mode (AND),
**then** I see only the tasks carrying both.
3. **Given** an active filter,
**when** I copy the URL and open it in another tab,
**then** the filter is applied the same way (state lives in the URL).
4. **Given** a filter that returns no tasks,
**then** I see an explanatory empty state with a
"Clear filters" button, not a blank list.
5. **Given** any filter,
**when** the number of results changes,
**then** it is announced via `aria-live` ("12 tasks visible of 47").Criterion 3 is the one with architectural consequences: it forces the filter to be part of the application state and of the router (06-04 and the router.js of Nómada Tasks), rather than a local variable of the view. A well-written acceptance criterion decides design.
Criterion 4 is the one most often forgotten in portfolio projects: empty states. An application that shows a blank area when there are no results looks broken even when it works perfectly.
6.3 Story H-11: seeing who is overloaded
### H-11 · See the team's workload
**As a** coordinator
**I want** to see how many open hours each person has per week
**so that** I can spread the work before somebody gets swamped
**Acceptance criteria**
1. **Given** a board with assigned tasks that have due dates,
**when** I open the workload report,
**then** I see a table of people by ISO weeks
with the open hours in each cell.
2. **Given** a person with more than 40 open hours in a week,
**then** their cell is highlighted visually **and** with text
(not by color alone), and the report states how many people are
above the limit (R7 + R15).
3. **Given** a task with no `dueDate`,
**then** its hours appear in a "No date" column
and are never spread arbitrarily across a week.
4. **Given** a task with subtasks,
**then** its hours are counted once (the leaves' hours),
never duplicated between parent and children (R12).Criterion 4 is the one that will cost you a whole afternoon if you do not write it now. Double-counting hours in trees is one of those bugs that visibly breaks nothing: it just makes every number wrong.
How many stories to write. For an MVP like this one, 12 to 18 stories is the right order of magnitude. Fewer than 10 usually means the stories are too coarse ("as a user I want to manage tasks" is not a story, it is a module). More than 25 usually means you are describing the implementation instead of the need.
- Prioritizing with MoSCoW
Having stories is not enough: you need to know in what order. MoSCoW is a simple and surprisingly effective prioritization method, named after the initials of its four categories:
| Category | Meaning | Rule of use |
|---|---|---|
| M · Must have | Without this there is no product. If it is missing, you do not ship | At most 60 % of the total effort |
| S · Should have | Important, but the product works without it | ~20 % of the effort |
| C · Could have | Desirable if there is time to spare | ~20 % of the effort |
| W · Won't have | Explicitly out of this version | It is the list from section 5 |
The 60 % rule is the important part and the one almost everyone breaks. If your Musts consume 100 % of the available time, you do not have a plan: you have a bet. Any unexpected event — and there is always one — leaves you with nothing to ship. The remaining 40 % in Should and Could is your slack: when time gets tight, they are sacrificed painlessly and the product is still shippable.
Full prioritization of the Orbita MVP:
| ID | Story | MoSCoW | Est. (h) | Milestone | Depends on |
|---|---|---|---|---|---|
| H-01 | See the task board | M | 6 | H2 | — |
| H-02 | Create a task with validation | M | 8 | H2 | H-01 |
| H-03 | Change status through valid transitions | M | 5 | H2 | H-01 |
| H-04 | Break down into subtasks | M | 10 | H3 | H-02 |
| H-05 | Assign an assignee | M | 4 | H3 | H-02 |
| H-06 | Edit and delete a task | M | 6 | H3 | H-02 |
| H-07 | Filter by several tags | M | 7 | H4 | H-01 |
| H-08 | Filter by assignee and status | M | 3 | H4 | H-07 |
| H-09 | Local persistence with migrations | M | 8 | H4 | H-02 |
| H-10 | Change history per task | S | 9 | H5 | H-06 |
| H-11 | Workload report per person and week | S | 8 | H5 | H-05 |
| H-12 | Empty state and error states | S | 4 | H5 | H-01 |
| H-13 | Full keyboard navigation | S | 5 | H5 | H-01 |
| H-14 | Export the report to CSV | C | 4 | H6 | H-11 |
| H-15 | Monthly calendar view | C | 12 | H6 | H-01 |
| H-16 | Installable as an offline PWA | C | 6 | H6 | H-09 |
Split: Must 57 h (57 %), Should 26 h (26 %), Could 22 h (22 %). Total 105 h. The 60 % rule is met with room to spare.
Two warnings about this table:
- The "Depends on" column is what defines the real order. MoSCoW says what is important; dependencies say what is possible. You cannot do H-11 before H-05, however much you want to.
- The estimates come from section 14, not out of thin air, and they already have the correction factor explained there baked in.
- The data model, written before you code
Here is the most profitable advice in the whole lesson: the data model is written before the code, in a document, and it is reviewed the way a contract is reviewed. Changing a field in a document takes thirty seconds. Changing it once it is in the domain, in the repository, in the view, in the tests and in users' saved data takes an afternoon and a migration (lesson 11-03).
Orbita's model extends the Nómada Tasks one with three new entities.
8.1 Entity diagram
erDiagram
USER ||--o{ TASK : "is assignee of"
USER ||--o{ TASK : "reviews"
USER ||--o{ CHANGE : "makes"
TASK ||--o{ TASK : "breaks down into"
TASK ||--o{ CHANGE : "accumulates"
TASK }o--o{ TAG : "carries"
BOARD ||--o{ TASK : "contains"
BOARD ||--o{ USER : "gathers"
USER {
string id PK
string name
string initials
string role "coordination|team|guest"
boolean active
}
TASK {
number id PK
string title
string assigneeId FK "null allowed"
string reviewerId FK "null allowed"
string priority "high|medium|low"
string status "pending|in-progress|done"
number estimatedHours
string dueDate "ISO or null"
number parentTaskId FK "null if root"
string createdAt "ISO"
}
TAG {
string name PK "lowercase"
}
CHANGE {
string id PK
number taskId FK
string userId FK
string date "ISO with time"
string field
string before
string after
}
8.2 The entities, field by field
User — the most important new entity, because it changes the type of two fields you spent ten modules treating as text.
| Field | Type | Rules | Note |
|---|---|---|---|
id |
string |
Unique, stable, never reused | Text rather than number: users may come from an external system in v2.0 |
name |
string |
Not empty after trim() |
Personal data: see the warning at the end of the section |
initials |
string |
1–3 characters, uppercase | For the text avatar, no images |
role |
string |
'coordination' | 'team' | 'guest' |
Closed set, like priority |
active |
boolean |
Defaults to true |
A user is never deleted: they are deactivated. See 8.3 |
Task — keeps the eight Nómada Tasks fields and adds four:
| Field | Change with respect to Nómada Tasks |
|---|---|
assignee → assigneeId |
Goes from string to a reference to User.id; still accepts null (R8) |
reviewer → reviewerId |
Same |
parentTaskId |
New. null if it is a root; otherwise the id of the parent task (E2) |
createdAt |
New. ISO date with time; needed for R4 and for ordering the history |
estimatedHours |
Now derived if the task has children: the sum of the leaves (R12) |
id |
Still a sequential number (R1), now unique across the whole tree |
Change — the history entity (E6). It is append-only: it is added to, never modified or deleted.
| Field | Type | Note |
|---|---|---|
id |
string |
A unique identifier; it can be crypto.randomUUID() (07-06) |
taskId |
number |
Which task it affects |
userId |
string |
Who did it |
date |
string ISO with time |
When. With a time, unlike dueDate |
field |
string |
Which field changed, or 'creation' / 'deletion' |
before / after |
string | null |
Values serialized to text so the entry is readable without context |
That last decision — serializing to text instead of storing the original value — is deliberate and it is worth understanding why: a history entry has to still be readable two years from now, when the model has changed and priority may no longer exist. A history that depends on the current model to be interpreted is not a history: it is a partial copy of the state.
8.3 The three modeling decisions you have to make now
1 · What happens when you delete a user who has tasks assigned?
Three options, and you have to pick one explicitly:
| Option | What it does | Consequence |
|---|---|---|
| Cascade delete | Deletes their tasks | Unacceptable: the team loses work |
Set the tasks to null |
Unassigns them | Acceptable, but historical information is lost |
| Deactivate (chosen) | active: false; their tasks keep the reference |
Everything is preserved; the interface shows "(inactive)" |
The chosen option is the third, and it is the one used in almost all professional software: actually deleting referenced entities is almost never the right thing. This decision deserves an ADR (lesson 11-06).
2 · Are tags an entity or an array of strings?
In Nómada Tasks they were an array of normalized strings (R9). For the Orbita MVP they stay that way, with a function that derives the tag catalog from the board. Turning them into an entity with its own id only pays off if you need to rename a tag across all tasks at once, and that is out of scope.
Write it down in the document: a decision made explicitly and justified is not technical debt; it is a decision. What creates debt is not noticing there was a choice.
3 · Is the tree stored nested or flat with a reference?
| Shape | How | Advantage | Drawback |
|---|---|---|---|
| Nested | task.subtasks = [...] |
Reads and renders naturally | Moving a subtask means cutting and pasting in two places; looking up by id forces a full traversal |
Flat with parentTaskId (chosen) |
Flat list; the tree is built on read | Lookup by id is direct; moving is changing one field; it saves and serializes trivially |
You have to build the tree in memory and detect cycles |
The flat shape is what databases use and what will save you trouble in lesson 11-03. Building the tree is a fifteen-line recursive function (03-07); keeping a duplicated nested structure in sync is a permanent problem.
Data protection notice. The moment your model contains people's
name, you are modeling personal data. In a learning project with fictional data there is no legal issue at all, but build the habit now: note in the scope document which fields are personal, what they are used for and how long they are kept. If one day this product handled real people's data, GDPR requires a legal basis, information for the data subject, minimization and retention periods — and that requires specific legal advice, not a JavaScript lesson. We will come back to it in 11-03 and 11-05.
- The new business rules: R11 to R15
Rules R1 to R10 from Nómada Tasks are still in force. I will run through them briefly because they are the base the new ones rest on:
| Rule | Description |
|---|---|
| R1 | The id is unique and sequential; the application assigns it, never the user |
| R2 | The title cannot be empty or contain only whitespace |
| R3 | estimatedHours greater than 0 and no higher than 40 |
| R4 | dueDate cannot be earlier than the creation date |
| R5 | A new task is always born in 'pending' status |
| R6 | Only the valid status transitions are allowed |
| R7 | Nobody can exceed 40 estimated hours assigned in the same week |
| R8 | A task with no assignee uses null, never '' |
| R9 | Tags are stored in lowercase and without duplicates |
| R10 | An overdue task (dueDate in the past and status other than 'done') is highlighted |
And these are the five new ones, the ones that give Orbita its character:
| Rule | Description | Where it is enforced | Error it throws |
|---|---|---|---|
| R11 | assigneeId and reviewerId must refer to an existing and active user, or be null. One person cannot be both assignee and reviewer of the same task |
Domain, on creation and on assignment | ValidationError with .field |
| R12 | The subtask tree allows no cycles and has a maximum depth of 3. The estimatedHours of a task with children are the sum of the leaves, never a value of its own |
Domain, on linking and on calculation | RuleError |
| R13 | A task with at least one unfinished subtask cannot move to 'done'. Closing a parent task records the closing of all its leaves |
Domain, in changeStatus |
RuleError |
| R14 | Every domain modification generates an immutable history entry. The history is not edited or deleted; a correction is a new entry | Application, after every mutation | — (invariant, not validation) |
| R15 | A user with the 'guest' role can only read. The R7 workload is calculated per user and ISO week, counting only open tasks and only the leaves' hours |
Domain (workload) and application (permissions) | RuleError |
Five observations about these rules, because each one hides a decision:
R11 and referential integrity. "Existing and active" is stronger than "existing". It means that when you deactivate a user you have to decide what happens to their future assignments: the chosen answer is that existing ones are preserved (they are history) but no new ones can be created. That asymmetry is intentional and deserves an explicit test.
R12 and the sum of the leaves. It is the rule that prevents the double-counting from criterion 4 of H-11. Its practical consequence: estimatedHours stops being an editable field as soon as a task has children, and the interface must reflect that (a disabled field with an explanation, not a field that accepts a value which is silently ignored).
R13 and cascading closure. Forbidding closing a parent with open children is the strict option; allowing it by closing the children automatically is the convenient one. The chosen rule does the strict thing by default and offers the cascade as an explicit action ("Also close the 3 pending subtasks"). Never cascade silently: deleting or closing things the user has not looked at is the recipe for distrust.
R14 and immutability. "A correction is a new entry" is the age-old accounting principle, and it is what makes a history worth anything. As soon as it can be edited, it stops being evidence of anything.
R15 and the ISO week. Choosing the ISO 8601 week (Monday to Sunday, with the Thursday rule for week 1) instead of "the last seven days" is a decision with consequences: you have to implement it properly, and it is a beautiful test case because 1 January sometimes falls in week 52 of the previous year. Note it down: it will be one of your parameterized tests in lesson 11-04.
Where the rules live. All of them, without exception, in the domain layer. The view may anticipate them to give the user good information (disabling a button, warning before submitting), but the view is never the one that decides. If a rule only exists in the form, it does not exist: calling the method from somewhere else is enough to bypass it. This is exactly what 06-07 explained about client-side validation, and the same thing lesson 11-05 will repeat about server-side validation.
- The layered architecture and the boundaries that are never crossed
Nómada Tasks had four folders — model/, data/, view/, util/ — and that organization was not aesthetic: it was a layered architecture, with rules about who is allowed to call whom. Orbita formalizes it with four layers plus one cross-cutting layer.
flowchart TD
subgraph APP["application/ — orchestrates use cases"]
A1["createTask()"]
A2["changeStatus()"]
A3["generateReport()"]
end
subgraph VIEW["view/ — DOM, events, accessibility"]
V1["BoardView"]
V2["TaskForm"]
V3["ReportView"]
end
subgraph DATA["data/ — persistence and network"]
D1["MemoryRepository"]
D2["LocalRepository"]
D3["ApiRepository"]
end
subgraph DOM["domain/ — rules R1-R15, no dependencies"]
M1["Task"]
M2["Board"]
M3["User"]
M4["rules.js"]
end
VIEW -->|"events"| APP
APP -->|"reads and writes"| DOM
APP -->|"saves and loads"| DATA
DATA -->|"rebuilds"| DOM
APP -->|"notifies state"| VIEW
style DOM fill:#dcfce7,stroke:#16a34a
style DATA fill:#dbeafe,stroke:#2563eb
style VIEW fill:#fef3c7,stroke:#d97706
style APP fill:#f3e8ff,stroke:#9333ea
The four layers, with their responsibility and their prohibitions:
| Layer | Responsibility | May import | Never imports |
|---|---|---|---|
domain/ |
The entities, their invariants and rules R1–R15 | Only pure util/ |
data/, view/, application/, fetch, document, localStorage |
data/ |
Save, load, talk to the network, migrate formats | domain/, util/ |
view/, application/ |
view/ |
Render, listen to events, accessibility, focus | util/, domain/ types for reading only |
data/, application/ (it receives functions, it does not import them) |
application/ |
Use cases: orchestrates domain + data + state and notifies the view | The three above | — |
util/ |
Pure functions: dates, formatting, debounce |
Nothing | Everything else |
The three boundaries that are never crossed, and what happens to you if you cross them:
-
domain/does not know a browser exists. Nodocument, nolocalStorage, nofetch, noalert. Immediate practical consequence: the domain is tested in Node without jsdom, in milliseconds, and that is what makes hundreds of fast tests possible (11-04). If one day you needDate.now()inside the domain, it gets passed in as a parameter or as an injected clock — never called directly, because then your tests depend on what day you run them. -
view/does not know where the data comes from. It receives a state and some functions (onCreateTask,onChangeStatus) and calls them. It does not know whether behind them there is memory,localStorageor an API. Practical consequence: changing the storage in lesson 11-03 will not touch a single view file. -
data/does not know what gets rendered. It returns domain entities or throws typed errors. It does not return HTML, or user messages, or "strings ready to display". Practical consequence: translating an error into friendly text is the view's responsibility, which is why the sameRuleErrorcan be shown in three different ways on three screens.
Why this architecture and not another. Two references from the course itself:
-
05-04 (modules) taught that an
importis a declared dependency: by writing it you are saying "this does not work without that". A layered architecture is, literally, a rule about whichimports are allowed. And because it is a mechanical rule, it can be automated: ESLint (08-02) has import-restriction rules that break the build if somebody writesimport { save } from '../data/...'insidedomain/. Configuring it is exercise 3. -
10-06 showed with data that
domain/rules.jswas identical across the four versions of the same screen (plain JavaScript, React, Vue and Angular). That fact is the decisive argument: if the domain does not depend on the view, it survives a change of view. And views change. Always.
A note of honesty: for a 100-hour project, four layers may look like ceremony. They are not, for a very concrete reason: the cost of introducing them at the start is one hour; the cost of introducing them when there are already 4,000 mixed-up lines is a week. And in this project in particular you need them, because lesson 11-03 is going to replace the entire data layer and 11-04 is going to test each layer with a different technique.
- The environment: repository and toolchain
Now, yes, keyboard. The goal of this section is a repository that, with a single command, verifies everything and comes out green.
11.1 Initialize
# 1 · Folder and repository
mkdir orbita && cd orbita
git init -b main
# 2 · package.json (no questions)
npm init -y
# 3 · Development and build environment
npm install --save-dev vite
# 4 · Code quality (08-02)
npm install --save-dev eslint @eslint/js prettier eslint-config-prettier
# 5 · Tests (08-03, 08-05)
npm install --save-dev jest jest-environment-jsdom \
@testing-library/dom @testing-library/user-event @testing-library/jest-dom
# 6 · End to end (08-06)
npm install --save-dev cypress
# 7 · Git hooks
npm install --save-dev husky lint-staged
npx husky initEach line, explained:
git init -b maincreates the repository with the main branch calledmainfrom the outset, which is what GitHub expects and what the deployment workflow in lesson 11-05 will use.viteis the dev server and the bundler. It gives you instant reload on save and, in production, the build with minification and hashing you will see in 11-05. It is the tool Nómada Tasks used from Module 9 onwards.- ESLint and Prettier separate responsibilities as 08-02 explained: ESLint looks for errors, Prettier enforces formatting.
eslint-config-prettierturns off the ESLint rules that would fight with Prettier. - Jest with
jsdomfor unit and integration tests; Testing Library to query the DOM by role and by text, like a person (08-05). - Cypress for the three end-to-end journeys (08-06).
- Husky and lint-staged run the checks before every commit, on the files you are committing. It is the difference between "CI will tell me" and "I cannot commit broken code".
11.2 The full package.json
{
"name": "orbita",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Work manager for small teams. Final project of the JavaScript course.",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"e2e": "cypress run",
"e2e:open": "cypress open",
"verify": "npm run lint && npm run format:check && npm run test:cov && npm run build"
},
"lint-staged": {
"*.js": ["eslint --fix", "prettier --write"],
"*.{css,html,json,md}": ["prettier --write"]
}
}The key script is verify. It is the project's contract in one line: lint, formatting, tests with coverage and build. It is exactly what continuous integration will run (11-04) and what you should be able to launch at any moment to know whether the project is healthy. The associated rule is simple and admits no exceptions:
npm run verifyhas to be green before everypush. Always.
The order of the four steps is not accidental: they go from fastest to slowest, so that the first one to fail is the cheapest to detect.
11.3 ESLint configuration with the architecture boundaries
This is the eslint.config.js file, and it contains the part that makes the architecture of section 10 enforce itself:
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
export default [
js.configs.recommended,
prettier,
// General project rules
{
files: ['src/**/*.js'],
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
globals: { window: 'readonly', document: 'readonly', localStorage: 'readonly' }
},
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
eqeqeq: ['error', 'always'],
'prefer-const': 'error'
}
},
// BOUNDARY 1: the domain knows nothing about the browser or the other layers
{
files: ['src/domain/**/*.js'],
languageOptions: {
globals: {} // no window, no document, no localStorage
},
rules: {
'no-restricted-imports': ['error', {
patterns: [
{ group: ['**/data/**'], message: 'The domain cannot import from data/.' },
{ group: ['**/view/**'], message: 'The domain cannot import from view/.' },
{ group: ['**/application/**'], message: 'The domain cannot import from application/.' }
]
}],
'no-restricted-globals': ['error',
{ name: 'document', message: 'The domain does not touch the DOM.' },
{ name: 'localStorage', message: 'The domain does not persist; that is data/.' },
{ name: 'fetch', message: 'The domain does not talk over the network; that is data/.' }
]
}
},
// BOUNDARY 2: the view knows nothing about persistence
{
files: ['src/view/**/*.js'],
rules: {
'no-restricted-imports': ['error', {
patterns: [
{ group: ['**/data/**'], message: 'The view receives data, it does not go looking for it.' }
]
}]
}
}
];What you have just done is important and deserves to be seen clearly: you have turned an architectural decision into an automatic rule. From now on the boundary does not depend on you remembering it on a Friday afternoon. If somebody — you, three weeks from now — writes import { LocalRepository } from '../data/local-repository.js' inside domain/task.js, npm run lint fails, the commit is rejected and CI goes red.
Architectures that cannot be verified automatically always erode. It is not a matter of discipline: it is a matter of nobody remembering a two-month-old document at eleven at night.
11.4 The pre-commit hook
Two steps: format and check only the files you are committing (lint-staged), and run only the affected tests (--onlyChanged). Deliberately fast: a slow hook is a hook you will end up skipping with --no-verify, and a hook that gets skipped is worth nothing. The full suite is CI's responsibility.
- The starting folder structure
orbita/
├── .github/workflows/ci.yml ← continuous integration (11-04)
├── .husky/pre-commit
├── cypress/
│ ├── e2e/ ← the 3 journeys (11-04)
│ └── support/
├── docs/
│ ├── scope.md ← MVP + what will NOT be done
│ ├── data-model.md ← entities + R1-R15
│ ├── stories.md ← stories with criteria
│ └── adr/ ← architecture decisions (11-06)
│ └── 0001-layered-architecture.md
├── public/
│ ├── manifest.json
│ └── icons/
├── src/
│ ├── main.js ← the single entry point: wires everything up
│ ├── styles/
│ │ ├── base.css
│ │ └── components.css
│ ├── domain/ ← NO external dependencies
│ │ ├── task.js
│ │ ├── user.js
│ │ ├── board.js
│ │ ├── tree.js ← recursive traversal (03-07)
│ │ ├── history.js
│ │ ├── rules.js ← R1-R15 in a single place
│ │ └── errors.js ← ValidationError, RuleError, DataError
│ ├── data/
│ │ ├── repository.js ← the CONTRACT (the boundary)
│ │ ├── memory-repository.js
│ │ ├── local-repository.js
│ │ ├── migrations.js ← (11-03)
│ │ └── seed.js ← fictional sample data
│ ├── application/
│ │ ├── state.js ← the single source of truth
│ │ ├── use-cases.js
│ │ └── events.js
│ ├── view/
│ │ ├── dom.js ← buildElement, $, $$
│ │ ├── board-view.js
│ │ ├── task-form.js
│ │ ├── report-view.js
│ │ ├── history-view.js
│ │ └── router.js
│ └── util/
│ ├── dates.js ← including the ISO week for R15
│ ├── format.js ← Intl
│ └── time.js ← debounce, throttle
├── test/
│ ├── domain/
│ ├── data/
│ └── view/
├── .gitignore
├── .prettierrc
├── eslint.config.js
├── index.html
├── jest.config.js
├── package.json
├── README.md
└── vite.config.jsThree decisions in this structure, with their rationale:
src/as the code root. It separates code from configuration, which already amounts to ten files at the root. Vite expects it that way by default.docs/versioned with the code. If the scope document lives in a loose note or an external service, it drifts out of sync within two weeks. Inside the repository, it changes in the same commit as the code it reflects.test/outsidesrc/, mirroring its structure. There is an alternative of putting tests next to the code (task.test.jsbesidetask.js); both work. The advantage of separating them is that the production build has nothing to exclude and per-layer coverage can be read at a glance.
- Git for real: branches, commits and reviews
Until now you may have used Git as a backup: git add ., git commit -m "changes", git push. That works until you need to answer one of these questions, and then it does not work at all:
- When did the workload report stop working and which change broke it?
- Why is this odd chunk that I do not dare touch written the way it is?
- Can I undo the subtasks feature without undoing the three that came after?
- What exactly went into the version I deployed on Tuesday?
Git history is documentation that writes itself if you give it thirty seconds per commit. These are the project's rules.
13.1 Short branches
One branch per user story. It is born from main, lives between a few hours and three days, and gets merged.
git switch -c feat/h04-subtasks # born from main
# ... work, several small commits ...
npm run verify # mandatory before pushing
git push -u origin feat/h04-subtasks
# review (13.3), merge, and delete the branchNaming convention, with the same prefixes as the commits:
| Prefix | What for | Example |
|---|---|---|
feat/ |
New functionality | feat/h07-tag-filter |
fix/ |
Fixing a bug | fix/duplicated-hours-in-tree |
refactor/ |
Internal change with no behavior change | refactor/extract-workload-rules |
docs/ |
Documentation only | docs/adr-0003-repository |
chore/ |
Tooling, dependencies, configuration | chore/update-vite |
Why short. A three-week branch accumulates conflicts, drifts away from main and reaches a point where merging it is scary. A two-day branch merges without a second thought. If a story does not fit in three days, the story is too big: split it (H-04 can be split into "create subtask", "aggregate hours" and "closing rule R13").
13.2 Conventional commits
The Conventional Commits format is a widely adopted standard:
<type>(<optional scope>): <description in the imperative, lowercase, no period>
<optional body: the WHY, not the what>
<optional footer: BREAKING CHANGE, references>Real examples from this project, from worst to best:
❌ fixes
❌ Changes to the board
❌ fix bug
✅ feat(domain): add subtask linking with cycle detection
✅ fix(report): count only the hours of the tree's leaves
✅ test(domain): cover R13 with the four status combinations
✅ refactor(view): extract row building into taskRow()
✅ docs(adr): record the decision to use a flat repository with parentTaskIdAnd a commit with a body, which is where the real value lives:
fix(report): count only the hours of the tree's leaves
The workload report was adding the parent task's hours plus those of its
subtasks, duplicating the total. With the sample board it gave 71 h
where the real total is 48 h.
The cause is that a parent's estimatedHours is derived (R12) but
accumulateWorkload() walked every flat task without distinguishing leaves.
Adds isLeaf() in domain/tree.js and filters before accumulating.
Regression test in test/domain/workload.test.js.
Refs: R12, H-11 criterion 4Nobody writes this to show off. You write it because six months from now, when the report gives an odd number again, this message shows up in git log and saves you two hours. It is the same argument as the R14 change history, applied to code instead of data.
The golden rule of commit size: if you need the word "and" to describe what it does, it is two commits.
What goes in the description and what goes in the body:
| Part | Content | Question it answers |
|---|---|---|
| Description | What changes, in the imperative, ≤ 72 characters | What |
| Body | Context, cause, discarded alternatives | Why |
| Footer | References to stories, rules, breaking changes | What it relates to |
The what is already in the diff. The why only exists in your head, and your head cannot be consulted six months from now.
13.3 Reviews, even when you work alone
"I review my own code" sounds like theater. It is not, if it is done with a procedure that forces a shift in perspective:
- Open a Pull Request against
main, always. Even if you are the one who will approve it. - Let time pass. Ideally until the next day; at minimum, an hour doing something else. An immediate review sees nothing because your head still fills in the gaps.
- Read the full diff in the web interface, not in your editor. The visual context switch is surprisingly effective: you will see things that were invisible in your editor.
- Apply the checklist from the next section and write the comments in the PR, not on a piece of paper.
- Fix with new commits on the same branch, not by rewriting. Let the correction be visible.
Review checklist (we will extend it in 11-02 and 11-06):
| # | Question | If the answer is "no"… |
|---|---|---|
| 1 | Does the change do one thing? | Split it |
| 2 | Is there a test that fails without this change? | Write it first |
| 3 | Does it respect the layer boundaries? | ESLint already told you; listen to it |
| 4 | Do the names say what they do? | Rename; it is cheap now |
| 5 | Is there commented-out code, console.log or an undated TODO? |
Delete it |
| 6 | Are the edge cases handled (empty, null, error)? |
Add them |
| 7 | Is it accessible by keyboard and with a screen reader? | Go back to the accessibility section |
| 8 | Does the commit message explain the why? | Rewrite it |
And a rule worth gold when somebody else reviews your code or you review theirs: criticize the code, never the person. "This function does three things" instead of "you made a mess". Apply it to yourself too: the goal is a better product, not a verdict.
- Milestones and honest estimation
14.1 The estimation problem
Everybody estimates badly and always in the same direction: too low. The cause is well studied and is called the planning fallacy: when we estimate we imagine the path where everything goes well, because it is the only one we can imagine in detail. Setbacks, by definition, cannot be enumerated.
The honest method has three steps:
- Estimate each story in hours of effective work, assuming there are no interruptions and that you know how to do it.
- Multiply by a correction factor according to how familiar the problem is to you.
- Add 20 % of buffer to the project total for whatever is in no story at all (configuring things, fixing the deployment, an odd Jest failure).
| Familiarity level | Factor | Example in Orbita |
|---|---|---|
| I have done it several times | × 1.3 | Form with validation (06-07) |
| I have done it once, guided | × 2 | Persistence with migrations (07-01, but the migrations are new) |
| I understand it but have never done it alone | × 3 | Subtask tree with rules R12/R13 |
| I do not know where to start | × 4 or research first | CSV export with special characters |
A full example, story H-04:
| Item | Hours |
|---|---|
| Naive estimate ("a tree, half a day") | 4 |
| Factor × 3 (you have never implemented a tree with business rules) | 12 |
| Downward adjustment: the MVP only exposes one level | 10 |
The 10 hours in the table in section 7 come from there, not from a hunch.
Orbita total: 105 h of stories + 21 h of buffer (20 %) = 126 hours. At 8 hours a week, about 16 weeks. If that seems like a lot, you have two legitimate ways out: put in more hours per week, or cut the Coulds (22 h) and land at 13 weeks. What is not a way out is deciding that it will really be 60 hours.
14.2 The milestones
A milestone is not a date: it is a state of the product that can be shown. Each one ends in something demonstrable.
| Milestone | What can be shown when it is finished | Stories | Hours | Lesson |
|---|---|---|---|---|
| H1 · Foundations | Green repository: npm run verify passes, with a trivial test and an empty deployable page |
— | 10 | 11-01 |
| H2 · Living domain | The domain with R1–R15 and its tests green, exercisable from the console | H-01…H-03 | 25 | 11-02 |
| H3 · Full vertical slice | Creating, viewing, assigning and breaking down a task end to end in the browser | H-04…H-06 | 24 | 11-02 |
| H4 · Data that lasts | Filters and local persistence with migrations; reloading loses nothing | H-07…H-09 | 22 | 11-03 |
| H5 · Usable product | History, report, empty states and keyboard navigation | H-10…H-13 | 26 | 11-04 |
| H6 · Published | Deployed with HTTPS, CI green, README and demo | H-14…H-16 | 19 | 11-05, 11-06 |
gantt
title Orbita — 16-week plan at 8 h/week
dateFormat YYYY-MM-DD
axisFormat W%W
section Foundations
H1 · Repository, tooling, CI :h1, 2026-09-21, 10d
section Domain
H2 · Entities, rules R11-R15, TDD :h2, after h1, 22d
section Product
H3 · Full end-to-end vertical slice :h3, after h2, 21d
H4 · Filters and persistence :h4, after h3, 19d
H5 · History, report, accessibility :h5, after h4, 23d
section Delivery
H6 · Deployment and documentation :h6, after h5, 17d
20 % buffer :res, after h6, 21d
Two observations about the Gantt chart:
- The buffer appears in the diagram. If the buffer is not drawn, it does not exist: the first setback will eat it and you will write the plan off. Drawn, it is a part of the plan being consumed, and you can see it.
- The milestones do not overlap. It is tempting to draw tasks in parallel, but you work alone: parallelism is an illusion whose only function is to make the plan look shorter.
How the plan is used. Not as a promise, but as a measuring instrument. At the end of each week, note the hours spent and the stories closed. If by the end of H2 you are 40 % over the projected time, your correction factor is too low: raise it for the rest of the plan instead of hoping to catch up. Recovering lost time almost never happens; adjusting the model does.
- The Definition of Done
The Definition of Done answers a question that sounds silly and is not: when is a story finished? With no written answer, "done" means "it works on my machine when I do what I expect", and that is the origin of 80 % of a personal project's debt.
It is written once, at the start, and applied to every story without exception:
## Definition of Done (v1.0)
A story is DONE when all of the following is true:
### Functionality
- [ ] All its acceptance criteria are met and have been checked by hand
- [ ] Edge cases have defined behavior: empty list, `null` value,
very long text, out-of-range number, network error
- [ ] The empty, loading and error states are implemented
### Code
- [ ] `npm run verify` is green
- [ ] It respects the layer boundaries (ESLint does not complain)
- [ ] No `console.log`, no commented-out code, no undated `TODO`
- [ ] Names are in the project's language and are consistent
### Tests
- [ ] Unit tests for the affected domain
- [ ] At least one integration test per acceptance criterion
that involves the interface
- [ ] Every business rule touched has a test for the case that
DOES pass and the case that does NOT
- [ ] Domain coverage does not drop below 90 %
### Accessibility
- [ ] Full keyboard navigation, with focus always visible
- [ ] Correct semantic elements (no clickable `<div>`s)
- [ ] Relevant changes are announced with `aria-live`
- [ ] Minimum contrast of 4.5:1 on normal text
- [ ] axe reports no serious-level issues
### Performance
- [ ] Within the budget from section 16
- [ ] No memory leaks after entering and leaving the screen three times
### Documentation
- [ ] README updated if installation or usage changes
- [ ] ADR written if an architectural decision has been made
- [ ] Commit message with the whyYes, it is long. And yes, it gets followed, for two reasons:
- Most of the boxes are checked by a machine:
npm run verifycovers seven of them. Only a few require you to do something by hand. - It is cheaper than the alternative. Every box on this list represents a real problem that shows up when it is not checked, and all of them are more expensive to fix later.
A practical tip: save it as a Pull Request template in .github/pull_request_template.md. That way it shows up by itself, rendered as a task list, every time you open a PR. What does not show up by itself does not get done.
- The performance and accessibility budget
Lesson 09-01 left behind an idea you have to apply now, not at the end: measure before optimizing. And its corollary, which is what this section is about: a goal without a number is not a goal.
Nómada Tasks measured its baseline at the end, with the application already built, and found ten problems all at once. You are going to do the opposite: set the budget now, with the application empty, and let CI watch over it from day one. The difference is enormous: a budget set at the start is broken on the day it breaks, by a single suspicious change; set at the end, it is broken by the accumulation of thirty changes and nobody knows which one did it.
16.1 Performance budget
The reference column holds the measured Nómada Tasks numbers from lesson 09-01 (baseline with 600 tasks, after all the Module 9 optimizations):
| # | Metric | How it is measured | Orbita budget | Nómada Tasks (ref.) |
|---|---|---|---|---|
| 1 | Initial JS (compressed) | Vite build / Network | ≤ 60 kB | 58.3 kB |
| 2 | Requests for the first screen | Network | ≤ 4 | 3 |
| 3 | LCP (simulated mobile, Slow 4G, 4× CPU) | Lighthouse CI | ≤ 2.5 s | 1.9 s |
| 4 | CLS | Lighthouse CI | ≤ 0.1 | 0.02 |
| 5 | INP when filtering | Performance, Interactions | ≤ 200 ms | 42 ms |
| 6 | render() with 500 tasks |
User Timing | ≤ 50 ms | 31 ms (600 tasks) |
| 7 | DOM nodes in the document | Performance, counter | ≤ 1,500 | 1,194 |
| 8 | Memory retained after 3 navigation cycles | Memory, 3 snapshots | ≈ 0 | no leaks |
| 9 | Lighthouse performance score | Lighthouse CI | ≥ 90 | — |
The budgets are somewhat looser than the Nómada Tasks numbers, and that is deliberate: Orbita has more screens and more functionality. An impossible budget is ignored in the first week; an achievable but demanding one is respected.
The budget rule: if a change breaks it, there are three legitimate ways out — optimize the change, drop the feature, or raise the budget consciously and write down why. The fourth way out, "I will look at it later", is what produces 2 MB applications.
16.2 Accessibility budget
| # | Criterion | How it is checked | Threshold |
|---|---|---|---|
| 1 | No serious axe issues | Automated axe in CI | 0 |
| 2 | Lighthouse accessibility score | Lighthouse CI | ≥ 95 |
| 3 | Everything reachable by keyboard | Manual, per screen | 100 % |
| 4 | Focus always visible | Manual + CSS :focus-visible |
Always |
| 5 | Normal text contrast | axe / DevTools | ≥ 4.5:1 |
| 6 | Large text and UI element contrast | axe / DevTools | ≥ 3:1 |
| 7 | Usable at 200 % zoom | Manual | No loss of content or function |
| 8 | Important changes announced | Manual with a screen reader | aria-live present |
| 9 | No information conveyed by color alone | Manual review | 0 cases |
Criterion 9 has an immediate consequence in Orbita and it is worth seeing now: the overdue task from R10 cannot be highlighted in red alone, and the overloaded cell from R7 cannot be highlighted in amber alone. Both need text or an icon with alternative text. If you know it now, you design it right; if you discover it in the accessibility review, you rebuild it.
Thresholds 1, 2, 5 and 6 are verified by the machine. The rest are manual and belong in the Definition of Done. Lesson 11-04 sets up the full automation.
- The
README.md template
README.md templateThe README.md is the first thing anybody landing on your repository sees: a reviewer, a recruiter, or you a year from now. It is created now, with gaps, and filled in as the project advances. A README written on the last day shows, and not in a good way.
# Orbita Work manager for small teams: tasks, subtasks, workload per person and change history. Final project of the JavaScript course, built **without frameworks**. 🔗 **Demo:** https://<user>.github.io/orbita/ 📸 *(screenshot or 10 s GIF here)* --- ## What problem it solves A team of three or four people needs to know who is doing what, what is blocked and who is overloaded. The big tools are overkill; a spreadsheet falls short as soon as there are rules. ## What it does - Tasks with status, priority, tags, assignee and due date - **Subtasks** with aggregated hours and progress - **Multiple filtering** by tags (*all* / *any* mode), assignee and status, with the filter saved in the URL - **Workload report** per person and ISO week, with an overload warning - **Immutable history** of every change - Works **offline** and installable as a PWA ## Technical decisions | Decision | Why | ADR | |---|---|---| | No framework | The product is a single SPA with rich domain logic; the cost of maintaining our own infrastructure is acceptable | [ADR-0002](docs/adr/0002-no-framework.md) | | Layered architecture | The domain must be testable without a browser and survive a change of view | [ADR-0001](docs/adr/0001-layered-architecture.md) | | Flat tree with `parentTaskId` | Lookup and move are O(1); it serializes trivially | [ADR-0003](docs/adr/0003-flat-tree.md) | | `localStorage` with numbered migrations | Expected volume < 1 MB; migrate without losing data | [ADR-0004](docs/adr/0004-persistence.md) | ## How to run it
git clone https://github.com/
npm test # unit and integration npm run test:cov # with coverage report npm run e2e # end-to-end journeys npm run verify # all of the above + lint + build
src/domain/ Rules R1-R15. No dependencies. Tested in Node. src/data/ Persistence and network. One interface, three implementations. src/view/ DOM, events and accessibility. Does not know where data comes from. src/application/ Use cases and state. Joins the three above.
## Known limitations - Single device: no synchronization between browsers (v2.0) - No authentication: data lives in the browser and **is not private** - Subtask depth limited to one level in the interface - Tested on recent Chrome, Firefox and Safari; no IE support ## Roadmap - [ ] v1.1 · Calendar view and CSV export - [ ] v1.2 · Three levels of subtasks - [ ] v2.0 · Backend with authentication and synchronization ## License MIT — see [LICENSE](LICENSE).
The sections almost nobody writes and that impress reviewers the most are "Technical decisions" and "Known limitations". The first demonstrates judgment; the second demonstrates honesty and awareness of the limits of your own work, which is exactly what is looked for in someone you are going to work with. Lesson 11-06 develops the README in depth.
Common Mistakes and Tips
Starting to code before the data model is written. It is the most expensive mistake of all, because it does not hurt at first: it hurts in week four, when you discover that assignee should have been a reference and it is already in twenty places. Half an hour of document saves two days of refactoring. Write the model, read it out loud, and only then open the editor.
Confusing MVP with "shoddy version". An MVP has fewer features, not less quality. The ones that are there are finished: tested, accessible and with their empty and error states. An application with three impeccable features demonstrates vastly more than one with twelve half-done — and in an interview, the second works against you.
Not writing the list of what will NOT be done. Without that list, every new idea seems reasonable because nothing says otherwise. With the list, adding something requires explicitly crossing it off, and that small act of friction filters out 90 % of Tuesday-afternoon ideas.
Estimating for the best case. Your naive estimate is the time it would take if everything went well and you already knew how to do it. Multiply by the factor in the table in section 14 without negotiating with yourself, and add the 20 % buffer. A plan that is met motivates you; one that is broken from week two gets abandoned.
Leaving the architecture as an intention. "I will be careful not to import data/ from domain/" is a sentence with a two-week expiry date. Configure the ESLint rule from section 11.3 on day one: turn your good intention into a build error, which is the only thing that does not get forgotten.
Deferring accessibility until "when it works". Adding accessibility at the end means rebuilding half the view: clickable <div>s have to become <button>s, focus has to be managed, announcements have to be introduced. Done from the start it costs practically nothing, because almost all of it comes down to using the right HTML element.
An empty README for months. Write the first version today, even if half of it is gaps. Every time you make a decision, add a row to the technical decisions table. Reconstructing the why of twenty decisions on the last day is impossible: you will not remember them.
Long branches. A three-week branch is a parallel project you will one day have to reconcile. If a story does not fit in three days, split it. If you do not know how to split it, that is a sign you do not understand it well enough yet — and that is valuable information too.
Tip · Write the scope document for "you, three months from now". That reader remembers nothing, has no context and only has ten minutes. If the document works for them, it works for you and it will work for anybody.
Tip · Set a fixed day and time to work, and protect them. The number one cause of abandoning personal projects is not technical difficulty: it is losing the rhythm. Two fixed three-hour sessions a week yield vastly more than "whenever I can".
Tip · Do the first deployment with the application empty. In milestone H1, before writing any logic. You will discover the configuration problems when they cost nothing, instead of in the last week with everything on the line. It is the same principle as the pilot phase of the migrations in 10-06.
Exercises
These three exercises are milestone H1 of your project. When you finish them you will have the scope document, the data model and a green repository: everything you need to start building in the next lesson.
Exercise 1 — The scope document.
Create docs/scope.md in your repository with these seven sections:
- The product in one sentence (the format from section 2): who uses it, what it does, what problem it solves.
- The MVP: the feature table with the "MVP?" column and its reason. At least 12 candidates, and at least 5 with "No".
- Out of scope: the three-column table from section 5, with at least 8 rows.
- User stories: between 12 and 18, using the As a / I want / so that template. At least four fully developed with Given / When / Then criteria (at least 3 criteria each), including at least one accessibility criterion.
- MoSCoW prioritization: the table from section 7 with estimate, milestone and dependencies. Verify the 60 % rule.
- Milestone plan: the six milestones with their demonstrable deliverable and their Mermaid Gantt chart, with the buffer drawn in.
- Definition of Done: adapted to your project, with at least 18 checkboxes spread across the six categories.
Exercise 2 — The data model and the rules.
Create docs/data-model.md with:
- An entity diagram in Mermaid (
erDiagram) with all your entities and their relationships. - A table per entity with field, type, whether it accepts
null, rules and a note justifying the chosen type. - The table of rules R1–R15 (or whichever correspond to your domain, if you chose another one) with: description, layer where it is enforced and error it throws.
- The three modeling decisions from section 8.3 applied to your domain, each with its discarded alternatives and the reasoning.
- A complete example of fictional data in JSON with at least 8 entities, including an edge case for each new rule.
- A "Personal data" section listing which fields are personal, what they are used for and how long they are kept.
Exercise 3 — The green repository.
Leave the repository in the exact state lesson 11-02 starts from:
- A repository with
main, a first conventional commit and a correct.gitignore(node_modules,dist,.env,coverage,cypress/videos). package.jsonwith the ten scripts from section 11.2, includingverify.eslint.config.jswith both architecture boundaries configured and checked..prettierrc,jest.config.js(withtestEnvironment: 'jsdom'and coverage thresholds) andvite.config.js.- A pre-commit hook with Husky and lint-staged, verified with a real commit.
- The folder structure from section 12, with a
.gitkeepin the empty ones. - A test that proves the boundaries work: write
src/domain/boundary-test.jswithimport ... from '../data/repository.js', check thatnpm run lintfails with your custom message, and delete it. Document the result in ADR-0001. README.mdwith the template from section 17 (the gaps get filled in later).npm run verifygreen.
Solutions
There are no solutions in the form of code, because the project is yours. These are the rubrics each deliverable is assessed against. Score yourself honestly: the goal is not the grade, it is spotting what is missing before it costs you dearly.
Rubric for exercise 1 — Scope document (30 points)
| Criterion | 0 · Insufficient | 1 · Acceptable | 2 · Good | 3 · Excellent |
|---|---|---|---|---|
| Product sentence | Does not exist or describes the implementation | Says what it does | Says who, what and what for | Also distinguishes it from obvious alternatives |
| MVP trimming | Everything is MVP | Something is discarded | ≥ 5 reasoned "No"s | Every "No" passes the question test from section 4 |
| Out of scope | Does not exist | A list of things | A table with reasons | Also with a realistic "when it gets reconsidered" |
| Stories | Vague or described as technical tasks | Correct format | ≥ 4 with verifiable criteria | The criteria translate directly into tests |
| Accessibility in criteria | Absent | Mentioned separately | ≥ 1 acceptance criterion | Present in every story with an interface |
| MoSCoW | Not prioritized | Categorized | With estimates and dependencies | Meets the 60 % rule and proves it with the arithmetic |
| Estimation | No correction factor | Uniform factor | Factor by familiarity | Also with the 20 % buffer drawn in the Gantt chart |
| Milestones | Dates only | With deliverables | Every milestone is demonstrable | Ordered by real dependency, with no fake overlaps |
| Definition of Done | Does not exist | Short list | ≥ 18 checkboxes across 6 categories | Most of them machine-verifiable |
| Usefulness to a third party | Incomprehensible without you | Understandable with effort | Understandable on its own | Somebody could continue it without talking to you |
Pass threshold: 20/30, with at least 2 points in "MVP trimming" and in "Stories". If you fall short there, everything else is built on sand.
Rubric for exercise 2 — Data model (24 points)
| Criterion | Insufficient (0) | Acceptable (1) | Good (2) | Excellent (3) |
|---|---|---|---|---|
| Entities | Relationships missing | All present | With correct cardinalities | Readable, annotated Mermaid diagram |
| Types | Ambiguous or "string" for everything | Defined | With justification | Also with explicit closed sets |
| Nullability | Unspecified | Marked | With consistent criteria | Distinguishes "not applicable" from "unknown" |
| New rules | Fewer than 3 | 5 rules stated | With layer and error assigned | Each with its edge case documented |
| Deletion decision | Not considered | Chosen | With discarded alternatives | With interface consequences described |
| Tree structure | Undecided | Chosen | Justified | With the cost of the operations analyzed |
| Sample data | Absent | Present | With edge cases | Directly reusable as a test seed |
| Personal data | Not mentioned | Identified | With a purpose | Also with retention period and minimization |
An unmistakable warning sign: if your rules table has fewer than five new rules specific to your domain, your product is a CRUD. Add hard rules — they are what makes the domain interesting, what gives the 11-04 tests meaning and what tells well in an interview.
Rubric for exercise 3 — Repository (binary verification)
There are no shades of gray here: each point is either met or not met.
| # | Check | Command | Expected |
|---|---|---|---|
| 1 | The repository exists and has main |
git branch --show-current |
main |
| 2 | The first commit is conventional | git log --oneline -1 |
Starts with chore: or feat: |
| 3 | .gitignore is correct |
git status --short |
No node_modules/ or dist/ |
| 4 | The scripts exist | npm run |
The 10 from section 11.2 |
| 5 | Lint passes | npm run lint |
No errors |
| 6 | Formatting is applied | npm run format:check |
No differences |
| 7 | The tests pass | npm test |
At least 1 test, 0 failures |
| 8 | The build works | npm run build |
dist/ generated |
| 9 | The hook fires | commit with a lint failure | The commit is rejected |
| 10 | The boundary works | add the forbidden import |
npm run lint fails with your message |
| 11 | Everything together | npm run verify |
Green |
Point 10 is the one that really matters and the one almost nobody checks. A badly configured ESLint rule does not fail: it simply does nothing, and you believe you are protected for three months. Trigger the error on purpose, check the message, and only then delete it.
Final self-assessment for milestone H1. Answer honestly before moving on to the next lesson:
| Question | Yes / No |
|---|---|
| Can I describe my product in one sentence without hesitating? | |
| Do I know exactly what I am NOT going to build? | |
| Could I write the first test for a rule today without inventing the model? | |
| Does my plan have a buffer, or have I assumed everything will go well? | |
Is npm run verify green right now? |
|
| Could I explain my layer boundaries to somebody else in two minutes? |
Six "yes"es and you can move on. A single "no" and it is worth going back before writing code, because every one of those "no"s multiplies into weeks of work later on.
Conclusion
You have done what almost nobody does in a personal project: planned before coding. And not with a decorative plan, but with instruments that will be used every week of this module.
You know that from here on you write the code: Nómada Tasks stops being the example handed to you solved and becomes the reference to compare against, with its six tasks, its 48 hours, its rules R1–R10 and its measured numbers. You have a default product — Orbita — with six extensions that force you to solve problems the course sidestepped: users as entities, a subtask tree, multiple filtering, a view that is not a list, a report that produces a file and an immutable history that grows without end. And you have three alternative domains — Capacity, Streak, Shelf — with the six extensions fully mapped, because the method is the same and what matters is that you care about it.
You know how to trim: the MVP is what cannot be removed without breaking the product sentence, and the test is a single question. Out of fifteen candidates, eight made it in. And you know that the half left out has to be written down, with its why and its when-it-gets-reconsidered, because a feature that is on no list is a feature that will show up on a Tuesday afternoon.
You know how to write user stories that say for whom and what for, with Given / When / Then criteria so precise that they translate into tests almost word for word — including the accessibility criteria, which go inside the story and not in a separate list reviewed at the end. And you know how to prioritize them with MoSCoW respecting the 60 % rule, which is what turns a plan into a plan and not a bet.
You have the data model written before the code, with its entity diagram, its justified types and the three decisions that had to be made explicitly: deactivating users instead of deleting them, tags as normalized text, and a flat tree with parentTaskId rather than a nested one. And you have the new rules R11 to R15 — referential integrity with active users, a cycle-free tree with hours from the leaves, closure blocked by open subtasks, an immutable history, and permissions with workload per ISO week — all living in the domain, because a rule that only exists in the form does not exist.
You have a layered architecture with three boundaries that are never crossed — the domain does not know there is a browser, the view does not know where data comes from, the data does not know what gets rendered — and, more importantly, you have them verified by ESLint: you have turned an intention into a build error, which is the only thing that does not erode. The argument is not theoretical: in 10-06, domain/rules.js was identical across the four versions of the same screen.
You have the environment set up: Vite, ESLint and Prettier, Jest with jsdom and Testing Library, Cypress, Husky and lint-staged, with a verify script that sums up the project's contract in one line and a rule with no exceptions. You have Git for real: short branches per story, conventional commits whose body explains the why — the only thing the diff cannot tell you six months from now — and reviews of your own code with distance, in the web interface and with a checklist.
You have an honest plan: 105 hours of stories, a correction factor by familiarity, 20 % buffer drawn in the Gantt chart, six milestones each ending in something that can be shown. You have a Definition of Done whose boxes are mostly checked by a machine, saved as a Pull Request template so that it shows up by itself. And you have the performance and accessibility budget set at the start, with the Nómada Tasks numbers as the reference: ≤ 60 kB, LCP ≤ 2.5 s, ≤ 1,500 nodes, 0 serious axe issues, 4.5:1 contrast and nothing communicated by color alone.
Milestone H1 is closed: scope document, data model with its rules, and a repository that answers green. The page is empty, and that is exactly right, because now you know what you are going to put on it and why.
What comes next is putting it there. And there is a way of doing that which is not the intuitive one — from the inside out, starting with the rules and ending with the pixels, and one complete feature before all of them half-done: it is Building the Project.
JavaScript Course: From Beginner to Advanced
Module 1: Introduction to JavaScript
- What Is JavaScript?
- Setting Up Your Development Environment
- Your First JavaScript Program
- JavaScript Syntax and Basic Concepts
- Variables and Data Types
- Basic Operators
- Type Conversion and Comparisons
- The Course Project: Nómada Tasks
Module 2: Control Structures
- Conditional Statements
- Loops: for, while, do-while
- Switch Statements
- Flow Control: break, continue and Nested Loops
- Error Handling with try-catch
Module 3: Functions
- Defining and Calling Functions
- Function Expressions and Arrow Functions
- Parameters and Return Values
- Scope and Closures
- Hoisting and the Execution Context
- Higher-Order Functions
- Recursion
Module 4: Objects and Arrays
- Introduction to Objects
- Object Methods and the
thisKeyword - Arrays: Basics and Methods
- Iterating over Arrays
- Searching, Sorting and Aggregating Data: find, sort and reduce
- Array Destructuring
- Object Destructuring, Spread and Rest
- JSON and Copying Objects
Module 5: Advanced Objects and Functions
- Prototypes and Inheritance
- Classes and Object-Oriented Programming
- Encapsulation: Getters, Setters and Private Fields
- Modules: Import and Export
- Asynchronous JavaScript: Callbacks
- Promises and Async/Await
- The Event Loop and the Microtask Queue
- Iterators and Generators
Module 6: The Document Object Model (DOM)
- Introduction to the DOM
- Selecting and Manipulating DOM Elements
- Handling Events
- Propagation, Delegation and Custom Events
- Creating and Removing DOM Elements
- Rendering Lists and HTML Templates
- Handling and Validating Forms
Module 7: Browser APIs and Advanced Topics
- Local and Session Storage
- The Fetch API and AJAX
- Robust Requests: Errors, Timeouts and AbortController
- WebSockets
- Service Workers and Progressive Web Apps (PWAs)
- Essential Browser APIs
- Introduction to WebAssembly
Module 8: Testing and Debugging
- Debugging JavaScript
- Code Quality: ESLint, Prettier and Conventions
- Unit Testing with Jest
- Test Doubles: Mocks, Stubs and Spies
- Integration Testing
- End-to-End Testing with Cypress
Module 9: Performance and Optimization
- Measure Before You Optimize: DevTools and Web Vitals
- Optimizing JavaScript Performance
- Memory Management
- Efficient DOM Manipulation
- Lazy Loading and Code Splitting
Module 10: JavaScript Frameworks and Libraries
- Why Frameworks Exist
- Introduction to React
- State Management with Redux
- Vue.js Basics
- Angular Basics
- Choosing the Right Framework
