Skip to content

How to Work on Lab Challenges

Labs ask campers to solve problems independently by satisfying a list of user stories. Campers may start with an empty or almost empty editor, or with existing code to debug or extend.

Unlike traditional user stories (“As a [role], I want [action], so that [benefit]”), user stories in freeCodeCamp labs describe specific, testable requirements for the camper’s code. They describe the expected result without giving step-by-step instructions for reaching it. Requirements may specify elements, properties, methods, or other implementation details when those are the concepts being practiced.

A good user story should be:

  • Specific: It describes one clear, testable behavior.
  • User-focused: It states what the camper needs to achieve, including any required implementation constraints, without walking through the solution.
  • Unambiguous: Any reasonable reader should interpret it the same way.
  • Testable: There must be a way to verify it with an automated test.

For example:

Too vagueClear and testable
“Make a navbar”“You should have a nav element that contains at least three links”
“Style the page nicely”“Your body element should have a font-family property set”

Every tested requirement must appear in the user stories. Some requirements may remain untested because of testing limitations.

Lab descriptions should have this format:

[introduction]
**Objective:** Fulfill the user stories below and get all the tests to pass to complete the lab.
**User Stories:**
1. You should have a `<element>` that [does something specific and testable].
2. Your `<element>` should have a [property/attribute] set to [value].
Note/Hint: (this is optional)

Labs reinforce previously learned concepts through independent problem solving.

Demo projects use the code under the --solutions-- heading in the Markdown file. They can go beyond the user stories to show what campers could create. Keep demos for introductory labs simpler to avoid overwhelming campers.

Labs that have a visual output (i.e. HTML-based) can have a demo project, in which case the frontmatter includes demoType: onClick. It is not mandatory, if the project design does not require it, it can be omitted.

You can create the lab with pnpm run create-new-project. This opens up a command line UI that guides you through the process. For chapter-based certifications, selecting the lab block label creates a lab template with the challenge type and seed files determined by your chosen content type. For example, a lab called lab-building-a-card would be created in curriculum/challenges/english/blocks/lab-building-a-card. The generator supports HTML-based, JavaScript, TypeScript, and Python labs.

The script will ask the following questions:

  1. Which certification does this belong to? - Select the certification (e.g., Responsive Web Design). This determines which superblock the lab is added to.
  2. What is the dashed name (in kebab-case) for this project? - The block name in kebab-case (e.g., lab-building-a-card). Must be unique across all blocks.
  3. Enter a title for this project: - The display title shown to campers. Defaults to the dashed name if left blank.
  4. Choose a help category - Select the most relevant category (e.g., HTML-CSS for HTML-based labs, JavaScript for JS labs).
  5. Choose a block label - Select lab. (This question only appears for chapter-based certifications.)
  6. Choose a block layout - Select link. (This question only appears for chapter-based certifications.)
  7. Choose a project content type - Select the language or file combination for the lab (e.g., HTML/CSS, JavaScript, TypeScript, or Python). This determines the challengeType and seed files. (Chapter-based certifications only.)
  8. What chapter should this project go in? - Select the chapter within the certification. (Chapter-based certifications only.)
  9. What module should this project go in? - Select the module within the chosen chapter. (Chapter-based certifications only.)
  10. At which position does this appear in the module? - The position number within the module (e.g., 1 for first project). (Chapter-based certifications only.)

For non-chapter-based certifications, instead of questions 5–10, you will be asked:

  • Which position does this appear in the certificate? - The position number in the certificate’s project list.

For these certifications, the script creates a generic HTML starter step, which must be adapted manually to create a lab.

Each lab is a single Markdown file. The challengeType and seed language depend on the type of lab:

challengeTypeLab typeSeed language(s)Solution language(s)
25HTML-basedhtml alone, or with css and optionally js, jsx, ts, or tsxsame combination
26JavaScript or TypeScriptjs or tssame language
27Pythonpypy

HTML-based, JavaScript/TypeScript, and Python labs use types 25, 26, and 27 for both ordinary labs and certification projects.

saveSubmissionToDB: true is used for certification projects. A project can appear as an ordinary lab in one certification while serving as a certification project in another, so it does not have to belong to a cert-project module in every superblock where it appears.

Here is the template for a JavaScript lab:

---
id: <ObjectId>
title: Build / Implement / Design / Debug [Name]
challengeType: 26
dashedName: lab-name
---
# --description--
**Objective:** Fulfill the user stories below and get all the tests to pass to complete the lab.
**User Stories:**
1. ...
# --hints--
Description of what the test checks.
```js
// test code
```
# --seed--
## --seed-contents--
```js
```
# --solutions--
```js
// solution code
```

Hooks are optional code blocks that run at specific points during test execution. They can be used to set up shared state, install fake timers, or clean up after tests.

Hooks are supported for lab types 25, 26, and 27.

Four hooks are available:

HookWhen it runs
--before-all--Once, before any test runs
--before-each--Before each individual test
--after-each--After each individual test (in a finally block, so it always runs even if the test fails)
--after-all--Once, after all tests have finished

Execution order:

For challenges that have an HTML file, --before-all-- is injected as a <script> tag into the sandboxed iframe before the user’s HTML is parsed, so it runs before user code. It has full access to DOM APIs and global test helpers like __FakeTimers and $ (jQuery). The user’s code then renders into the page, and each test runs against the live DOM:

1. --before-all--
2. user code (evaluated once)
3. (for each test): --before-each-- → test → --after-each--
4. --after-all--

For JavaScript and TypeScript challenges without an HTML file, --before-all-- runs once before any test. Each test evaluates --before-each--, the compiled user code, and the test together:

1. --before-all--
2. (for each test): --before-each-- → user code → test → --after-each--
3. --after-all--

For Python challenges, hooks are JavaScript code. Each test gets fresh Python globals. The evaluator first evaluates --before-each-- and the JavaScript test wrapper. If the wrapper returns an object with a test function, the evaluator then runs the user’s Python code with runPython and calls that function:

1. --before-all--
2. (for each test): --before-each-- → evaluate test wrapper → user code → test() → --after-each--
3. --after-all--

Plain JavaScript assertions without a test function run while the wrapper is evaluated and do not automatically execute the user’s Python code.

For both kinds of challenges without HTML, variables declared with let or const in --before-all-- are not available in tests or --after-all--, because these run in separate evaluations. To share JavaScript state, assign it to globalThis and remove it during cleanup. For example:

# --before-all--
```js
globalThis.testState = { count: 0 };
```
# --after-all--
```js
delete globalThis.testState;
```

Syntax:

Hooks use the same # --hook-name-- heading syntax as other sections. Each hook must contain exactly one code block. Hooks are placed after --description-- and before --hints--:

The following fake-timer example is for HTML-based challenges only. __FakeTimers is not available in the JavaScript or Python workers.

# --description--
...
# --before-all--
```js
// Runs once before any test. Set up shared state here.
let clock = __FakeTimers.install();
```
# --before-each--
```js
// Runs before each test.
```
# --after-each--
```js
// Runs after each test, even if it fails.
```
# --after-all--
```js
// Runs once after all tests. Clean up here.
clock.uninstall();
```
# --hints--
...

After you’ve committed your changes, check here for how to open a Pull Request.