MongoDB Peer-Programming: Building Features Live
We believe this is one of MongoDB's most distinctive interview rounds, and it's reported most often for full-stack, Node.js, and JavaScript-leaning roles, and variations show up across many of MongoDB's developer-tools and user-facing product teams (Compass, the VS Code extension, mongosh, and friends).
The format: a 90-minute live session, you share your screen, and you build features on a small app you already have running locally. Two MongoDB engineers pair with you. They're collaborators, not silent judges. It is not a LeetCode round. It's: can you take a small TypeScript/React app, extend it calmly, explain your trade-offs, and collaborate in real time?
The project you'll build on
MongoDB sends you a real, public starter project ahead of time, drawn from its public mongodb-js GitHub repositories. The exact repo can vary from loop to loop, so we'll rehearse on one of their real example projects, the mongodb-js/electron-template: you download it, run it, and during the interview you build features on top of it live.
📦 Do this before your interview day:
- Get the template:
github.com/mongodb-js/electron-template(download themainzip and extract it). - In a terminal, from the project folder:
npm installnpm run test(the tests should pass)npm run start(the app should launch a window)
- Make sure you can share your screen, and that the app + tests still run.
The stack is TypeScript + React + CSS + Electron (Node). You don't need deep React. The React docs cover everything you'll use. (You may swap React for another framework, but only if you set it up in advance and tests still run. We'll assume React, which is what the template ships with.)
Know the template cold
The template is intentionally tiny. That's good news. The whole point is that you understand it in minutes and start adding features, not that you decode a huge app. Below is the exact starter repo in our explorer, every file, byte-for-byte from main. Click around the tree:
Explorer
1import React, { useState } from 'react';2 3import Button from './components/button/Button';4import { sum } from './modules/sum';5 6import './styles.css';7 8export const App: React.FunctionComponent = ({}) => {9 const [ count, setCount ] = useState(0);10 11 return (12 <div>13 <h1 className="title">Title</h1>14 <div>15 Count: {count}16 </div>17 <div>18 <Button19 className="add-one-button"20 onClick={() => setCount(count => sum(count, 1))}21 >Add one</Button>22 </div>23 </div>24 );25}26 27export default App;What each file is doing
It looks like a lot of files, but only a handful carry meaning. Group them in your head like this:
The Electron main process (the desktop shell):
src/main.ts, the Node entry point. It creates aBrowserWindow, loadsindex.htmlinto it, and opens the DevTools. ThenodeIntegration: true/contextIsolation: falsesettings are permissive for a starter template, a great thing to flag out loud as "not how I'd ship a hardened app," but fine here.src/index.html, the shell page Electron loads. The only line that matters is<div id="app"></div>. That's where React mounts.
The renderer (your React app):
src/renderer.tsx, the browser-side entry point. It grabs#appand callscreateRoot(...).render(<App />). This is the bridge from "a blank HTML div" to "a running React tree."src/App.tsx, the only UI file, and where you'll spend ~all your time. Out of the box it's a counter: localuseState, a title, and one button.src/components/button/Button.tsx, a small, reusable presentational component. It owns no state; it just takeschildren,className, andonClick. This is the template showing you the pattern it wants you to follow when you add components.src/modules/sum.ts, a pure helper (sum(a, b)), deliberately kept out of the components so it's trivial to unit-test. Another pattern hint: pure logic lives inmodules/.src/styles.css, global CSS, imported straight intoApp.tsx.
Build & config (you rarely touch these, but know what they do):
package.json, scripts are the thing to read:npm run startcleansdist, runs webpack, then runs webpack in watch mode and launches Electron together (viaconcurrently).npm run testruns Jest.webpack.config.jsexports two configs,webpack.electron.js(targetelectron-main, buildsmain.ts→dist/main.js) andwebpack.react.js(targetelectron-renderer, buildsrenderer.tsx→dist/renderer.js, and injects it intoindex.html). Two bundles, one for each process.tsconfig.json,babel.config.js, TypeScript + Babel/JSX setup.jsx: "react-jsx"is why you don't import React just to use JSX.
Tests (Jest + React Testing Library):
test/App.spec.tsxrenders<App />and asserts the title is visible.test/modules/sum.spec.tsunit-testssum.test/setup.tswires up@testing-library/jest-dom.jest.config.jsmaps.ts/.tsxthroughts-jest,.jsthroughbabel-jest, and stubs CSS imports withidentity-obj-proxy.
The one-sentence mental model
If they say "walk us through it," say this and you'll sound organized:
"There are two bundles: the Electron main process and the React renderer. package.json runs webpack, which builds both into dist; Electron launches dist/main.js; main.ts creates a BrowserWindow and loads index.html; renderer.tsx mounts App into the #app div. The current UI is a tiny React component with local state and one reusable button, and tests use Jest + React Testing Library."
The three files that explain almost the whole runtime are src/main.ts (creates the window), src/renderer.tsx (mounts React), and src/App.tsx (the UI). Know those cold; skim the rest.
What's deliberately missing (and likely to be asked)
The template has no list rendering, forms, search, async loading, routing, or custom hooks. So the feature prompts you get are almost always "add one of those." The sequence below is built around exactly those gaps.
⚙️ A real gotcha worth knowing: npm run start runs webpack in watch mode, but there's no hot reload. Your files rebuild on save, but the open window keeps showing the old UI until you refresh it with Cmd/Ctrl + R (renderer changes) or restart the app (main-process changes). If a change doesn't appear, don't panic, just say "this template rebuilds but doesn't hot-reload, so I'll refresh the window," and press Cmd+R.
How the round actually flows
They hand you small, escalating feature prompts, each one building on the last. You implement, narrate as you go, and they help if you get stuck. The bar for a senior hire is less "React wizardry" and more: structured problem-solving, sensible state decisions, clean incremental changes, and clear communication.
We believe the exact prompt varies from session to session, so don't expect the wording below word-for-word. The shape of the round, though, is consistent: you take a small app and grow a list-and-form UI one feature at a time. So rather than guess the precise prompt, we'll work through a representative sequence that exercises every skill this round tends to probe. For each task: the prompt, a quick plan, the code (in the explorer, with changed lines highlighted and modified files dotted in the tree), and what the interviewer is really checking.
Our running feature: a Collection Manager
The textbook version of this exercise is a humble to-do list, and we believe a close cousin of it is what tends to show up here: an item tracker, a contacts list, a notes panel. The theme shifts from candidate to candidate; the underlying React does not. Whatever the surface, the thing being measured is the same: can you manage state across a list and a form, with a few small components?
So we'll build the version MongoDB engineers would actually recognise, a tiny Collection Manager, the kind of thing MongoDB Compass does. We'll manage the collections in a database:
- create a collection (with a name),
- drop one,
- search collections by name,
- rename a collection,
- mark which collections are indexed, and
- filter by index status.
Every one of those is a thin MongoDB-flavoured wrapper around a core React skill the round likes to test. If your real prompt turns out to be a to-do app or an address book instead, the moves are identical; you're just renaming things. Let's go.
Task 1: Turn the counter into a Collection Manager
Prompt: Replace the counter with a small UI: a text input and a Create collection button that appends the typed name to a list shown on the page. Clear the input after creating, and ignore empty input.
Plan (say it out loud first): "I'll keep the collections in useState as a string array, add a controlled input for the draft name, validate that it's non-empty on create, and render the list with .map. I'll keep it all in App.tsx for now and refactor once it grows."
Explorer
1import React, { useState } from 'react';2 3import Button from './components/button/Button';4 5import './styles.css';6 7export const App: React.FunctionComponent = () => {8 const [collections, setCollections] = useState<string[]>([]);9 const [newName, setNewName] = useState('');10 11 function handleCreateCollection() {12 const trimmed = newName.trim();13 if (trimmed === '') {14 return; // ignore empty / whitespace-only names15 }16 // Functional update: the next list is derived from the previous one.17 setCollections((current) => [...current, trimmed]);18 setNewName('');19 }20 21 return (22 <div>23 <h1 className="title">Collection Manager</h1>24 25 <input26 placeholder="New collection name"27 value={newName}28 onChange={(event) => setNewName(event.target.value)}29 />30 <Button31 className="create-collection-button"32 onClick={handleCreateCollection}33 >34 Create collection35 </Button>36 37 <ul>38 {collections.map((name, index) => (39 <li key={index}>{name}</li>40 ))}41 </ul>42 </div>43 );44};45 46export default App;Two small details that signal seniority:
- Trim then validate. A common bug is checking
newName === ''before trimming, then" "(spaces) sneaks in as an empty collection. Trim first, validate second. - Functional state update.
setCollections((current) => [...current, trimmed])derives the next list from the previous one, the safe default whenever new state depends on old state. (DirectsetCollections([...collections, trimmed])is usually fine for a single update too, a good thing to mention you know the difference.)
What the interviewer is really checking: controlled inputs (value + onChange), immutable list updates (new array, not .push), basic validation, and that you keep it simple. Resist the urge to over-engineer, a working, readable version first is the winning move. Then say "let me refresh the window to verify," hit Cmd+R, and create a couple of collections to show it works.
Task 2: Drop a collection
Prompt: Add a Drop button next to each collection that removes it from the list.
Plan: "To remove a specific row safely I need a stable identity per collection, not the array index, indexes shift when items are removed. I'll upgrade each collection from a bare string to an object with a unique id and a name, generate ids with a useRef counter, and filter by id on drop."
This is the moment to introduce a tiny data model. Narrate why: "Index-based keys and index-based deletes get buggy the instant the list reorders or filters. A stable id makes every later feature, search, rename, indexes, straightforward."
Explorer
1import React, { useRef, useState } from 'react';2 3import Button from './components/button/Button';4 5import './styles.css';6 7type Collection = {8 id: number;9 name: string;10};11 12export const App: React.FunctionComponent = () => {13 const [collections, setCollections] = useState<Collection[]>([]);14 const [newName, setNewName] = useState('');15 const nextId = useRef(0);16 17 function handleCreateCollection() {18 const trimmed = newName.trim();19 if (trimmed === '') {20 return;21 }22 const collection: Collection = { id: nextId.current++, name: trimmed };23 setCollections((current) => [...current, collection]);24 setNewName('');25 }26 27 function handleDropCollection(id: number) {28 setCollections((current) => current.filter((collection) => collection.id !== id));29 }30 31 return (32 <div>33 <h1 className="title">Collection Manager</h1>34 35 <input36 placeholder="New collection name"37 value={newName}38 onChange={(event) => setNewName(event.target.value)}39 />40 <Button41 className="create-collection-button"42 onClick={handleCreateCollection}43 >44 Create collection45 </Button>46 47 <ul>48 {collections.map((collection) => (49 <li key={collection.id}>50 <span>{collection.name}</span>51 <Button52 className="drop-collection-button"53 onClick={() => handleDropCollection(collection.id)}54 >55 Drop56 </Button>57 </li>58 ))}59 </ul>60 </div>61 );62};63 64export default App;What changed and why it matters:
type Collection = { id; name }. Promoting the model fromstringto an object is the single highest-leverage decision in the whole session, every feature after this leans on it.useReffor ids. AuseRefcounter gives monotonically increasing ids without triggering re-renders (it's not UI state). Mention you could usecrypto.randomUUID()instead, both are fine;useRefkeeps it dependency-free and deterministic.- Filter, don't splice.
current.filter((c) => c.id !== id)returns a new array and never mutates state. key={collection.id}. Now that rows have stable ids, the Reactkeybecomes stable too, no more index keys.
What the interviewer is really checking: do you reach for a stable identity instead of array indexes? This is the classic "junior vs. senior" tell in a list UI. Saying the words "index keys break when the list reorders or filters, so I'll give each collection an id" scores real points.
Task 3: Search collections by name
Prompt: Add a search box. As the user types, show only the collections whose name contains the query (case-insensitive). An empty query shows everything.
Plan: "The key decision: I will not store the filtered list in state. I'll store only the search text, and compute the visible list during render from my two sources of truth, collections and searchText. That way the filtered view can never drift out of sync when I add or drop a collection."
This is the most important idea in the whole round, so say it explicitly: derived state.
Explorer
1import React, { useRef, useState } from 'react';2 3import Button from './components/button/Button';4 5import './styles.css';6 7type Collection = {8 id: number;9 name: string;10};11 12export const App: React.FunctionComponent = () => {13 const [collections, setCollections] = useState<Collection[]>([]);14 const [newName, setNewName] = useState('');15 const [searchText, setSearchText] = useState('');16 const nextId = useRef(0);17 18 function handleCreateCollection() {19 const trimmed = newName.trim();20 if (trimmed === '') {21 return;22 }23 const collection: Collection = { id: nextId.current++, name: trimmed };24 setCollections((current) => [...current, collection]);25 setNewName('');26 }27 28 function handleDropCollection(id: number) {29 setCollections((current) => current.filter((collection) => collection.id !== id));30 }31 32 // Derived state: never stored in useState, always computed during render33 // from the source-of-truth state (collections + searchText).34 const normalizedSearch = searchText.trim().toLowerCase();35 const visibleCollections =36 normalizedSearch === ''37 ? collections38 : collections.filter((collection) =>39 collection.name.toLowerCase().includes(normalizedSearch)40 );41 42 return (43 <div>44 <h1 className="title">Collection Manager</h1>45 46 <input47 placeholder="New collection name"48 value={newName}49 onChange={(event) => setNewName(event.target.value)}50 />51 <Button52 className="create-collection-button"53 onClick={handleCreateCollection}54 >55 Create collection56 </Button>57 58 <input59 placeholder="Search collections"60 value={searchText}61 onChange={(event) => setSearchText(event.target.value)}62 />63 64 <ul>65 {visibleCollections.map((collection) => (66 <li key={collection.id}>67 <span>{collection.name}</span>68 <Button69 className="drop-collection-button"70 onClick={() => handleDropCollection(collection.id)}71 >72 Drop73 </Button>74 </li>75 ))}76 </ul>77 </div>78 );79};80 81export default App;Why this is the senior answer:
- One source of truth.
collectionsis the data;searchTextis the query. The visible list is a pure function of those two, so it's recomputed every render and is always correct. If you instead keptvisibleCollectionsinuseState, you'd have to remember to re-filter it on every create, drop, and rename, and you'd eventually forget one. That bug is exactly what they're probing for. - Normalize once. Compute
searchText.trim().toLowerCase()a single time, then compare against each lowercased name. Don't re-lowercase inside the loop more than you need. - Empty query short-circuits. When the query is empty, return
collectionsas-is, no needless filtering, and the user sees everything.
What the interviewer is really checking: the derived-state instinct. The phrase to land is "I'll keep the filtered list as derived state computed during render, not as its own useState, so it can't get out of sync." If you remember one sentence from this entire guide, make it that one.
Task 4: Handle the empty states
Prompt: When there are no collections at all, show a friendly message instead of a blank area. And when a search matches nothing, say so.
Plan: "Two distinct empty states, and they're not the same thing. 'No collections exist yet' is a first-run state; 'your search matched nothing' is a filtered state. I'll branch on collections.length first, then on visibleCollections.length."
Explorer
1import React, { useRef, useState } from 'react';2 3import Button from './components/button/Button';4 5import './styles.css';6 7type Collection = {8 id: number;9 name: string;10};11 12export const App: React.FunctionComponent = () => {13 const [collections, setCollections] = useState<Collection[]>([]);14 const [newName, setNewName] = useState('');15 const [searchText, setSearchText] = useState('');16 const nextId = useRef(0);17 18 function handleCreateCollection() {19 const trimmed = newName.trim();20 if (trimmed === '') {21 return;22 }23 const collection: Collection = { id: nextId.current++, name: trimmed };24 setCollections((current) => [...current, collection]);25 setNewName('');26 }27 28 function handleDropCollection(id: number) {29 setCollections((current) => current.filter((collection) => collection.id !== id));30 }31 32 const normalizedSearch = searchText.trim().toLowerCase();33 const visibleCollections =34 normalizedSearch === ''35 ? collections36 : collections.filter((collection) =>37 collection.name.toLowerCase().includes(normalizedSearch)38 );39 40 return (41 <div>42 <h1 className="title">Collection Manager</h1>43 44 <input45 placeholder="New collection name"46 value={newName}47 onChange={(event) => setNewName(event.target.value)}48 />49 <Button50 className="create-collection-button"51 onClick={handleCreateCollection}52 >53 Create collection54 </Button>55 56 <input57 placeholder="Search collections"58 value={searchText}59 onChange={(event) => setSearchText(event.target.value)}60 />61 62 {collections.length === 0 ? (63 <p className="empty-state">No collections yet. Create your first one above.</p>64 ) : visibleCollections.length === 0 ? (65 <p className="empty-state">No collections match your search.</p>66 ) : (67 <ul>68 {visibleCollections.map((collection) => (69 <li key={collection.id}>70 <span>{collection.name}</span>71 <Button72 className="drop-collection-button"73 onClick={() => handleDropCollection(collection.id)}74 >75 Drop76 </Button>77 </li>78 ))}79 </ul>80 )}81 </div>82 );83};84 85export default App;Why this is more than a nicety: mixing the two empty states is a real bug. If you only check visibleCollections.length === 0, then a user who has 20 collections but typed a non-matching search sees "No collections yet. Create your first one above", which is confusing and wrong. Branching on the raw collections first, then the filtered list, keeps each message truthful.
What the interviewer is really checking: that you think about states beyond the happy path. Saying "there are actually two different empty states here, and I don't want to conflate them" shows product sense, not just coding.
Task 5: Rename a collection, and split the UI into components
Prompt: Let the user rename a collection inline, click Rename, edit the name in a text field, then Save or Cancel. Also,
App.tsxis getting big; tidy it up.
This is two moves at once, so narrate them separately.
Plan, part 1, the rename interaction: "I'll track which row is being edited with an editingId, plus the in-progress editingName. A row renders either its normal view or an edit field depending on whether its id matches editingId. Save trims, updates that one collection immutably with .map, and clears edit mode."
Plan, part 2, the refactor: "I'll lift the Collection type into its own file, and split the list into a CollectionList (maps the rows) and a CollectionRow (one row, knows how to render view-vs-edit). App stays the single source of truth for state and passes data + callbacks down as props."
Explorer
1import React, { useRef, useState } from 'react';2 3import Button from './components/button/Button';4import CollectionList from './components/collection-list/CollectionList';5import { Collection } from './types/Collection';6 7import './styles.css';8 9export const App: React.FunctionComponent = () => {10 const [collections, setCollections] = useState<Collection[]>([]);11 const [newName, setNewName] = useState('');12 const [searchText, setSearchText] = useState('');13 const [editingId, setEditingId] = useState<number | null>(null);14 const [editingName, setEditingName] = useState('');15 const nextId = useRef(0);16 17 function handleCreateCollection() {18 const trimmed = newName.trim();19 if (trimmed === '') {20 return;21 }22 const collection: Collection = { id: nextId.current++, name: trimmed };23 setCollections((current) => [...current, collection]);24 setNewName('');25 }26 27 function handleDropCollection(id: number) {28 setCollections((current) => current.filter((collection) => collection.id !== id));29 }30 31 function handleStartRename(collection: Collection) {32 setEditingId(collection.id);33 setEditingName(collection.name);34 }35 36 function handleSaveRename() {37 const trimmed = editingName.trim();38 if (trimmed === '') {39 return;40 }41 setCollections((current) =>42 current.map((collection) =>43 collection.id === editingId ? { ...collection, name: trimmed } : collection44 )45 );46 setEditingId(null);47 setEditingName('');48 }49 50 function handleCancelRename() {51 setEditingId(null);52 setEditingName('');53 }54 55 const normalizedSearch = searchText.trim().toLowerCase();56 const visibleCollections =57 normalizedSearch === ''58 ? collections59 : collections.filter((collection) =>60 collection.name.toLowerCase().includes(normalizedSearch)61 );62 63 return (64 <div>65 <h1 className="title">Collection Manager</h1>66 67 <input68 placeholder="New collection name"69 value={newName}70 onChange={(event) => setNewName(event.target.value)}71 />72 <Button73 className="create-collection-button"74 onClick={handleCreateCollection}75 >76 Create collection77 </Button>78 79 <input80 placeholder="Search collections"81 value={searchText}82 onChange={(event) => setSearchText(event.target.value)}83 />84 85 {collections.length === 0 ? (86 <p className="empty-state">No collections yet. Create your first one above.</p>87 ) : visibleCollections.length === 0 ? (88 <p className="empty-state">No collections match your search.</p>89 ) : (90 <CollectionList91 collections={visibleCollections}92 editingId={editingId}93 editingName={editingName}94 onEditingNameChange={setEditingName}95 onStartRename={handleStartRename}96 onSaveRename={handleSaveRename}97 onCancelRename={handleCancelRename}98 onDrop={handleDropCollection}99 />100 )}101 </div>102 );103};104 105export default App;The senior signals here:
- State stays lifted. The edit state (
editingId,editingName) lives inApp, not insideCollectionRow. The rows are controlled. They receiveisEditingand call callbacks up. That keeps one source of truth and makes "only one row edits at a time" fall out for free. - Components are presentational.
CollectionListandCollectionRowown no business state; they render props and forward events. That's the same pattern the template's ownButtondemonstrates. You're following the codebase's conventions, which interviewers love. - A type file.
src/types/Collection.tsis now imported byApp,CollectionList, andCollectionRow, one definition, no drift. - Immutable rename.
.map(c => c.id === editingId ? { ...c, name: trimmed } : c)updates one item without mutating the array or the object.
What the interviewer is really checking: can you decompose a growing component without over-engineering, and do you keep state lifted to a sensible owner? Don't refactor too early, doing it now, when a real second reason (rename) appears, is the right time. Say "I'll split this now that there's a clear seam, and keep all the state in App so the rows stay simple."
Task 6: Mark which collections are indexed
Prompt: Each collection can be indexed or not. Show that status, and let the user toggle it with an Add index / Drop index button.
Plan: "This is a new piece of data, so it belongs on the model. I'll add indexed: boolean to the Collection type and default it to false on create. Then a toggle handler flips it immutably, and the row shows a badge plus a contextual button label."
Explorer
1import React from 'react';2 3import Button from '../button/Button';4import { Collection } from '../../types/Collection';5 6type CollectionRowProps = {7 collection: Collection;8 isEditing: boolean;9 editingName: string;10 onEditingNameChange: (name: string) => void;11 onStartRename: (collection: Collection) => void;12 onSaveRename: () => void;13 onCancelRename: () => void;14 onToggleIndex: (id: number) => void;15 onDrop: (id: number) => void;16};17 18export const CollectionRow: React.FunctionComponent<CollectionRowProps> = ({19 collection,20 isEditing,21 editingName,22 onEditingNameChange,23 onStartRename,24 onSaveRename,25 onCancelRename,26 onToggleIndex,27 onDrop,28}) => {29 if (isEditing) {30 return (31 <li>32 <input33 className="rename-input"34 value={editingName}35 onChange={(event) => onEditingNameChange(event.target.value)}36 />37 <Button className="save-rename-button" onClick={onSaveRename}>38 Save39 </Button>40 <Button className="cancel-rename-button" onClick={onCancelRename}>41 Cancel42 </Button>43 </li>44 );45 }46 47 return (48 <li>49 <span>{collection.name}</span>50 {collection.indexed && <span className="badge-indexed">indexed</span>}51 <Button52 className="toggle-index-button"53 onClick={() => onToggleIndex(collection.id)}54 >55 {collection.indexed ? 'Drop index' : 'Add index'}56 </Button>57 <Button className="rename-button" onClick={() => onStartRename(collection)}>58 Rename59 </Button>60 <Button className="drop-collection-button" onClick={() => onDrop(collection.id)}>61 Drop62 </Button>63 </li>64 );65};66 67export default CollectionRow;Notice how many files move for one feature, and that the dots in the tree make it obvious: the type changes, App gains a handler, and the prop threads through CollectionList into CollectionRow. That's worth a sentence out loud:
Data vs. UI state, the distinction to verbalize: "indexed is real data about a collection, so it lives on the Collection model, not in component state. The button label (Add index vs Drop index) is pure UI derived from that data, so I compute it inline rather than storing it." That single sentence separates candidates who understand state from those who just use it.
And the prop threading is a perfect place to show senior judgment: "I'm prop-drilling onToggleIndex through CollectionList here. That's fine at this size, if it grew, I'd reach for composition or a context, but I won't add that complexity yet."
A touch of CSS makes the badge read well, the kind of small polish you'd add in src/styles.css:
.badge-indexed {
margin: 0 8px;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
color: #00684a; /* MongoDB forest green */
background-color: #c3f4d7; /* light spring-green */
}
Task 7: Filter by index status (and unit-test the logic)
Prompt: Add All / Indexed / Not indexed filter buttons that combine with the existing search. While you're at it, can you make the filtering logic testable?
This is the climax, and it ties the room together. Pull the filtering out of the component into a pure function, then write a Jest test for it, mirroring exactly what the template already does with sum.ts + sum.spec.ts.
Plan: "I'll add an indexFilter state, a small union type 'all' | 'indexed' | 'not-indexed'. Then I'll move all filtering into a pure filterCollections(collections, { search, indexFilter }) in modules/, so the component just calls it. Pure logic in modules/ is trivial to unit-test, so I'll add a spec that covers search, index filtering, and the two combined."
Explorer
1import { Collection } from '../types/Collection';2 3export type IndexFilter = 'all' | 'indexed' | 'not-indexed';4 5export type CollectionFilters = {6 search: string;7 indexFilter: IndexFilter;8};9 10// Pure function: same inputs always produce the same output, no React,11// no side effects. That is exactly what makes it trivial to unit-test.12export function filterCollections(13 collections: Collection[],14 filters: CollectionFilters15): Collection[] {16 const normalizedSearch = filters.search.trim().toLowerCase();17 18 return collections.filter((collection) => {19 const matchesSearch =20 normalizedSearch === '' ||21 collection.name.toLowerCase().includes(normalizedSearch);22 23 const matchesIndex =24 filters.indexFilter === 'all' ||25 (filters.indexFilter === 'indexed' && collection.indexed) ||26 (filters.indexFilter === 'not-indexed' && !collection.indexed);27 28 return matchesSearch && matchesIndex;29 });30}Why this is a Strong-Hire move:
- Pure function, no React.
filterCollectionstakes inputs and returns output, no hooks, no side effects. That's why it's testable, and saying so out loud is the point. - It composes. Search and index-filter combine with a simple
matchesSearch && matchesIndex. One function, one place to reason about "what's visible." - You wrote a test unprompted-ish. The round rewards testing. Following the template's own
modules/ + *.spec.tspattern shows you read the codebase and respect its conventions. Runnpm run testand show it green. - Union type over booleans.
'all' | 'indexed' | 'not-indexed'makes illegal states unrepresentable, better than two loosely-related booleans.
What the interviewer is really checking: do you know how to make logic testable by separating it from the UI? The sentence that lands: "I'll lift the filtering into a pure function in modules/, the same way the template keeps sum testable, then the component just calls it and I can unit-test every branch."
Task 8: Load the collections from a real source
Prompt: Right now the list starts empty. Load the initial collections from an async source instead, and handle loading and errors.
This is the most common "where does it go next" escalation, because real MongoDB tools fetch from a database. Keep it honest about the three states every async read has: loading, error, and loaded.
Plan: "I'll add a loadCollections() module that returns a Promise, a stand-in for a driver/API call. App calls it once in a useEffect on mount, tracking isLoading and loadError. I render a loading line, an error-with-retry, or the app. I'll also seed my nextId past the largest loaded id so new collections don't collide."
Explorer
1import { Collection } from '../types/Collection';2 3// Stand-in for a real data source. In production this is where you would4// call the MongoDB Node driver (db.listCollections()) or your own API.5const SEED: Collection[] = [6 { id: 1, name: 'users', indexed: true },7 { id: 2, name: 'orders', indexed: false },8 { id: 3, name: 'products', indexed: true },9];10 11export function loadCollections(): Promise<Collection[]> {12 return new Promise((resolve, reject) => {13 setTimeout(() => {14 // Flip this to true to exercise the error + retry path in the UI.15 const shouldFail = false;16 if (shouldFail) {17 reject(new Error('the database did not respond'));18 return;19 }20 resolve(SEED);21 }, 600);22 });23}The details that read as senior:
- All three states, explicitly. Loading, error (with a Retry that just calls
load()again), and success. New grads usually forget the error path, handling it is a differentiator. useEffect(..., [])for "on mount." Mention you know the empty dependency array means "run once after first render," and that React 18 StrictMode double-invokes effects in dev (so the fetch may fire twice locally, harmless here, but you'd guard real mutations).- Seed the id counter.
nextId.current = max(loaded ids) + 1avoids id collisions between loaded and newly-created collections, a subtle correctness point most people miss. - Honest about the stand-in. Say "in production this
loadCollectionsis where I'd call the MongoDB Node driver'slistCollections()or our API. I'm faking the latency so the UI states are real."
What actually earns a "Strong Hire"
After all the code, what they're really grading is mostly how you worked, not how much you finished. The signals MongoDB engineers consistently reward:
✅ Strong-Hire signals
- You narrate. A short plan before each task, and a running commentary while coding. Silence is the most common way strong coders underperform here.
- State decisions are deliberate. Stable ids over indexes; derived state over stored; data on the model, UI computed from it; state lifted to a sensible owner.
- Small, working increments. Each step compiles and runs. You refresh (
Cmd+R), verify, then move on. - You collaborate. You ask clarifying questions, accept hints gracefully, and think out loud so your pair can help.
- You respect the codebase. Reusing
Button, putting pure logic inmodules/, adding a*.spec.ts, following conventions instead of fighting them. - You know what you'd do next. "I'd add an active-filter style / debounce the search / persist to disk / use a context if this grew", naming trade-offs without necessarily building them.
🚩 No-Hire signals to avoid
- Coding in silence, or jumping to a keyboard before stating a plan.
- Storing derived data in
useStateand then fighting to keep it in sync. - Index-based keys/deletes, or mutating state with
.push/ direct assignment. - Over-engineering early (reducers, contexts, abstractions) before there's a reason.
- Going quiet when stuck instead of saying "here's what I'm thinking, here's where I'm unsure."
Your 90-minute game plan
- Minutes 0–5: Give the one-sentence mental model, point at
main.ts/renderer.tsx/App.tsx, and confirm the app + tests run. - Each task: Restate it → one-line plan → code in small steps →
Cmd+Rand verify → say what you'd do next. - When stuck: think out loud and invite your pair in. It's a peer-programming round, collaboration is the thing being measured.
- If offered a test: take it. A green
npm run teston a pure module is a quiet, strong finish.
You're not being asked to be a React savant. You're being asked to take a tiny, well-structured app and grow it calmly, correctly, and out loud, in MongoDB's own vocabulary. Build the Collection Manager a few times until the moves feel automatic, and you'll walk in ready. 🚀