| name | freecodecamp-curriculum |
| description | Comprehensive guide for contributing to and working with freeCodeCamp's open-source codebase and curriculum platform |
| triggers | ["help me contribute to freeCodeCamp","how do I add a curriculum challenge to freeCodeCamp","set up freeCodeCamp locally","how does freeCodeCamp's challenge system work","create a new freeCodeCamp certification","freeCodeCamp development environment setup","how to write freeCodeCamp challenge tests","freeCodeCamp codebase structure"] |
freeCodeCamp Curriculum & Platform Development
Skill by ara.so — Daily 2026 Skills collection.
freeCodeCamp.org is a free, open-source learning platform with thousands of interactive coding challenges, certifications, and a full-stack curriculum. The codebase includes a React/TypeScript frontend, Node.js/Fastify backend, and a YAML/Markdown-based curriculum system.
Architecture Overview
freeCodeCamp/
├── api/ # Fastify API server (TypeScript)
├── client/ # Gatsby/React frontend (TypeScript)
├── curriculum/ # All challenges and certifications (YAML/Markdown)
│ └── challenges/
│ ├── english/
│ │ ├── responsive-web-design/
│ │ ├── javascript-algorithms-and-data-structures/
│ │ └── ...
│ └── ...
├── tools/
│ ├── challenge-helper-scripts/ # CLI tools for curriculum authoring
│ └── ui-components/ # Shared React components
├── config/ # Shared configuration
└── e2e/ # Playwright end-to-end tests
Local Development Setup
Prerequisites
- Node.js 20+ (use
nvm or fnm)
- pnpm 9+
- MongoDB (local or Atlas)
- A GitHub account (for OAuth)
1. Fork & Clone
git clone https://github.com/<YOUR_USERNAME>/freeCodeCamp.git
cd freeCodeCamp
2. Install Dependencies
pnpm install
3. Configure Environment
cp sample.env .env
Key .env variables to set:
MONGOHQ_URL=mongodb://127.0.0.1:27017/freecodecamp
GITHUB_ID=$GITHUB_OAUTH_CLIENT_ID
GITHUB_SECRET=$GITHUB_OAUTH_CLIENT_SECRET
JWT_SECRET=$YOUR_JWT_SECRET
SESSION_SECRET=$YOUR_SESSION_SECRET
SENDGRID_API_KEY=$SENDGRID_API_KEY
4. Seed the Database
pnpm run seed
5. Start Development Servers
pnpm run develop
pnpm run develop:api
pnpm run develop:client
Curriculum Challenge Structure
Challenges are stored as YAML/Markdown files under curriculum/challenges/.
Challenge File Format
---
id: bd7123c8c441eddfaeb5bdef
title: Comment Your JavaScript Code
challengeType: 1
forumTopicId: 16783
dashedName: comment-your-javascript-code
---
Comments are lines of code that JavaScript will intentionally ignore.
```js
// This is an in-line comment.
/* This is a multi-line comment */
--instructions--
Try creating one of each type of comment.
--hints--
hint 1
assert(code.match(/(\/\/)/).length > 0);
hint 2
assert(code.match(/(\/\*[\s\S]+?\*\/)/).length > 0);
--seed--
--seed-contents--
--solutions--
### Challenge Types
| Type | Value | Description |
|------|-------|-------------|
| HTML | 0 | HTML/CSS challenges |
| JavaScript | 1 | JS algorithm challenges |
| JSX | 2 | React component challenges |
| Vanilla JS | 3 | DOM manipulation |
| Python | 7 | Python challenges |
| Project | 5 | Certification projects |
| Video | 11 | Video-based lessons |
---
## Creating a New Challenge
### Using the Helper Script
```bash
# Create a new challenge interactively
pnpm run create-challenge
# Or use the helper directly
cd tools/challenge-helper-scripts
pnpm run create-challenge --superblock responsive-web-design --block css-flexbox
Manual Creation
- Find the correct directory under
curriculum/challenges/english/
- Create a new
.md file with a unique ID
node -e "const {ObjectID} = require('mongodb'); console.log(new ObjectID().toString())"
- Follow the challenge file format above
Validate Your Challenge
pnpm run test:curriculum
pnpm run test:curriculum -- --challenge <challenge-id>
pnpm run test:curriculum -- --block basic-javascript
Writing Challenge Tests
Tests use a custom assertion library. Inside # --hints-- blocks:
JavaScript Challenges
# --hints--
`myVariable` should be declared with `let`.
```js
assert.match(code, /let\s+myVariable/);
The function should return true when passed 42.
assert.strictEqual(myFunction(42), true);
The DOM should contain an element with id main.
const el = document.getElementById('main');
assert.exists(el);
### Available Test Utilities
```js
// DOM access (for HTML challenges)
document.querySelector('#my-id')
document.getElementById('test')
// Code inspection
assert.match(code, /regex/); // raw source code string
assert.include(code, 'someString');
// Value assertions (Chai-style)
assert.strictEqual(actual, expected);
assert.isTrue(value);
assert.exists(value);
assert.approximately(actual, expected, delta);
// For async challenges
// Use __helpers object
const result = await fetch('/api/test');
assert.strictEqual(result.status, 200);
API Development (Fastify)
Route Structure
import { type FastifyPluginCallbackTypebox } from '../helpers/plugin-callback-typebox';
import { Type } from '@fastify/type-provider-typebox';
export const exampleRoutes: FastifyPluginCallbackTypebox = (
fastify,
_options,
done
) => {
fastify.get(
'/example/:id',
{
schema: {
params: Type.Object({
id: Type.String()
}),
response: {
200: Type.Object({
data: Type.String()
})
}
}
},
async (req, reply) => {
const { id } = req.params;
return reply.send({ data: `Result for ${id}` });
}
);
done();
};
Adding a New API Route
import { exampleRoutes } from './routes/example';
await fastify.register(exampleRoutes, { prefix: '/api' });
Database Access (Mongoose)
import mongoose from 'mongoose';
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
completedChallenges: [
{
id: String,
completedDate: Number,
solution: String
}
]
});
export const User = mongoose.model('User', userSchema);
Client (Gatsby/React) Development
Adding a New Page
import React from 'react';
import { Helmet } from 'react-helmet';
import { useTranslation } from 'react-i18next';
const MyNewPage = (): JSX.Element => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t('page-title.my-new-page')} | freeCodeCamp.org</title>
</Helmet>
<main>
<h1>{t('headings.my-new-page')}</h1>
</main>
</>
);
};
export default MyNewPage;
Using the Redux Store
import { createSelector } from 'reselect';
import { RootState } from './types';
export const userSelector = (state: RootState) => state.app.user;
export const completedChallengesSelector = createSelector(
userSelector,
user => user?.completedChallenges ?? []
);
import { useAppSelector } from '../redux/hooks';
import { completedChallengesSelector } from '../redux/selectors';
const MyComponent = () => {
const completedChallenges = useAppSelector(completedChallengesSelector);
return <div>{completedChallenges.length} challenges completed</div>;
};
i18n Translations
{
"my-component": {
"title": "My Title",
"description": "My description with {{variable}}"
}
}
const { t } = useTranslation();
t('my-component.title');
t('my-component.description', { variable: 'value' });
Testing
Unit Tests (Jest)
pnpm test
pnpm --filter api test
pnpm --filter client test
pnpm --filter client test -- --watch
Curriculum Tests
pnpm run test:curriculum
pnpm run test:curriculum -- --superblock javascript-algorithms-and-data-structures
pnpm run lint:curriculum
E2E Tests (Playwright)
pnpm run test:e2e
pnpm run test:e2e -- e2e/learn.spec.ts
pnpm run test:e2e -- --ui
Writing E2E Tests
import { test, expect } from '@playwright/test';
test('user can complete a challenge', async ({ page }) => {
await page.goto('/learn/javascript-algorithms-and-data-structures/basic-javascript/comment-your-javascript-code');
await page.locator('.monaco-editor').click();
await page.keyboard.type('// inline comment\n/* block comment */');
await page.getByRole('button', { name: /run the tests/i }).click();
await expect(page.getByText('Tests Passed')).toBeVisible();
});
Key pnpm Scripts Reference
pnpm run develop
pnpm run develop:api
pnpm run develop:client
pnpm run build
pnpm run build:api
pnpm run build:client
pnpm test
pnpm run test:curriculum
pnpm run test:e2e
pnpm run lint
pnpm run lint:curriculum
pnpm run seed
pnpm run seed:certified-user
pnpm run create-challenge
pnpm run clean
Superblock & Block Naming Conventions
Superblocks map to certifications. Directory names use kebab-case:
responsive-web-design/
javascript-algorithms-and-data-structures/
front-end-development-libraries/
data-visualization/
relational-database/
back-end-development-and-apis/
quality-assurance/
scientific-computing-with-python/
data-analysis-with-python/
machine-learning-with-python/
coding-interview-prep/
the-odin-project/
project-euler/
Block directories within a superblock:
responsive-web-design/
├── basic-html-and-html5/
├── basic-css/
├── applied-visual-design/
├── css-flexbox/
└── css-grid/
Common Patterns & Gotchas
Challenge ID Generation
Every challenge needs a unique 24-character hex ID:
import { ObjectId } from 'bson';
export const generateId = (): string => new ObjectId().toHexString();
Adding Forum Links
Every challenge needs a forumTopicId linking to forum.freecodecamp.org:
forumTopicId: 301090
Curriculum Meta Files
Each block needs a _meta.json:
{
"name": "Basic JavaScript",
"dashedName": "basic-javascript",
"order": 0,
"time": "5 hours",
"template": "",
"required": [],
"isUpcomingChange": false,
"isBeta": false,
"isLocked": false,
"isPrivate": false
}
Testing with Authentication
import { authedUser } from './fixtures/authed-user';
test.use({ storageState: 'playwright/.auth/user.json' });
test('authenticated action', async ({ page }) => {
await page.goto('/settings');
await expect(page.getByText('Account Settings')).toBeVisible();
});
Troubleshooting
MongoDB Connection Issues
mongosh --eval "db.adminCommand('ping')"
brew services start mongodb-community
MONGOHQ_URL=mongodb://127.0.0.1:27017/freecodecamp-test pnpm test
Port Conflicts
lsof -i :3000
kill -9 <PID>
Curriculum Validation Failures
pnpm run test:curriculum -- --verbose
Node/pnpm Version Mismatch
node --version
pnpm --version
nvm use
Client Build Errors
pnpm --filter client run clean
pnpm run develop:client
Contributing Workflow
git checkout -b fix/challenge-typo-in-basic-js
pnpm run test:curriculum
pnpm test
pnpm run lint
git commit -m "fix(curriculum): correct typo in basic-javascript challenge"
git push origin fix/challenge-typo-in-basic-js
Commit message prefixes: fix:, feat:, chore:, docs:, refactor:, test:
Resources