Recommended resources
The freely available resources this lesson will direct you to.
Hello! Last time, you turned a vague request about bad record IDs into observable acceptance criteria and a code-aware plan. This lesson takes the next step: implementing business rules yourself, without asking an AI tool to write the function.
You will work on a small batch preflight function for an integration service. It will decide which record IDs are usable before any downstream work begins. This is a separate illustrative feature, not a change to the single-request HTTP contract from the previous lesson. Allow about 40 minutes, with most of that time reserved for writing and checking your own JavaScript.
Turn the rules into a function contract
Suppose an integration receives a batch of candidate record IDs. Before another part of the service uses them, it needs a clean list. Your function will be named prepareRecordIds and will accept one argument, candidateIds.
Here are the complete business rules:
- The argument must be an array. For any other value, return exactly
{ ok: false, error: 'INVALID_BATCH' }. - For an array, inspect entries in their original order. A valid entry is a string with at least one non-whitespace character after trimming.
- Put each valid, trimmed ID in the result once. Keep its first-occurrence order. Duplicates are compared after trimming and are case-sensitive:
"C42"and"c42"are different IDs. - Count invalid entries and duplicates separately. An invalid entry is not also a duplicate.
- For an array, return an object with exactly these fields:
ok: true,ids,invalidCount, andduplicateCount. An empty array succeeds with an emptyidsarray and both counts at zero. - Do not change the input array, read external state, or perform I/O. Each call must produce its result solely from its argument.
For example, given [' C42 ', 'C42', null, 'c42', ' ', 12, ' D9 ', 'D9'], the result should contain ids: ['C42', 'c42', 'D9'], invalidCount: 3, and duplicateCount: 2. The full result also needs ok: true.
Before coding, distinguish decisions the rules make from ones they leave open. Here, you do not need to decide whether numbers should be converted to strings or whether capitalization should be standardized: the rules already say non-strings are invalid and comparisons are case-sensitive. Nor should you quietly turn a missing argument into an empty batch; a missing argument is not an array.
The following brief video segment offers a useful technique for getting from a specification to code. Its example uses a user interface, but concentrate on its advice to write small steps or pseudocode before dealing with syntax.
Translate Ideas To Code Using Problem Solving
Watch “Translate Ideas To Code Using Problem Solving” by Joy of Code for its approach to starting with a small building block and outlining uncertain steps before coding.
Watch the starting approach. Ignore the user-interface details; focus on how pseudocode turns a large-sounding task into decisions you can implement one at a time.
For prepareRecordIds, a useful outline is: classify the top-level input, prepare a fresh result for a valid batch, inspect entries in order, and return the finished result. That is a plan, not the implementation. You still need to decide where each count changes and when an ID becomes eligible to add.
Keep the decision separate from its effects
A pure function gives the same result for the same input values and does not change anything observable outside itself. That makes it a good fit for preflight business logic: the function can decide which IDs are ready, while a different part of the service later decides whether to send them downstream.
Watch how the video distinguishes receiving a collection as an argument from being free to modify that collection.
Learn Pure Functions In 10 Minutes
Watch “Learn Pure Functions In 10 Minutes” by Web Dev Simplified to see the two requirements for purity and a common mistake when a function receives an array.
Start with the definition. Then watch the array example: passing an array into a function removes one hidden dependency, but changing that array is still a side effect. Notice, too, why using a changing external value prevents repeatable results.
This distinction matters for your implementation. Calling push on a new local ids array is fine: that array did not exist before the call and will be returned as part of the result. Calling a mutating method on candidateIds, or replacing its entries while trimming them, changes data owned by the caller.
“Same result” means the returned objects have the same contents for the same input values; it does not require returning the same object instance on repeated calls. In fact, a fresh result prevents one caller from changing what a later caller receives.
Write the function yourself
You already have the JavaScript fundamentals needed here. If you want a quick syntax refresher, read the short portions of Functions below, particularly how arguments become parameters and how return hands a value back to the caller.
Read “Functions” from javascript.info for a focused reminder of parameters and return values before you implement the contract.
Under “Parameters,” read the explanation, beginning with “We can pass arbitrary data” and ending at the paragraph that distinguishes parameters from arguments; stop before “Default values.” Under “Returning a value,” read the return explanation, from its opening sentence through the explanation that reaching return stops the function and sends the value to its caller. You do not need the browser-based examples elsewhere on the page.
Now create prepare-record-ids.js and start with only this declaration:
function prepareRecordIds(candidateIds) {
// Write your implementation here.
}
Spend roughly 15–20 minutes on your own implementation. Do not request or paste AI-generated code for this function. The point is to build the link between a written rule and a line you can explain. You may use the reading above to check JavaScript syntax.
Work in short passes rather than trying to perfect the whole function at once:
- First make an array of already-valid, distinct strings produce the required result shape.
- Then handle the non-array argument and invalid entries.
- Finally handle trimming, duplicates, and preservation of the input array.
Useful language tools include Array.isArray, typeof, string .trim(), and array methods you already know. Check a value’s type before calling .trim() on it—the same ordering problem appeared in the service traced earlier. Resist adding a database call, logging, HTTP status code, or downstream adapter call: none belongs in this function’s contract.
Check behavior, then check purity
Before running your code, predict the result for an empty array, a missing argument, and two strings that become equal only after trimming. Then place these checks below your handwritten function in the same file:
const assert = require('node:assert/strict');
assert.deepStrictEqual(prepareRecordIds(undefined), {
ok: false,
error: 'INVALID_BATCH',
});
assert.deepStrictEqual(prepareRecordIds([]), {
ok: true,
ids: [],
invalidCount: 0,
duplicateCount: 0,
});
assert.deepStrictEqual(
prepareRecordIds([' C42 ', 'C42', null, 'c42', ' ', 12, ' D9 ', 'D9']),
{
ok: true,
ids: ['C42', 'c42', 'D9'],
invalidCount: 3,
duplicateCount: 2,
},
);
const input = [' A1 ', 'A1'];
const before = [...input];
const first = prepareRecordIds(input);
const second = prepareRecordIds(input);
assert.deepStrictEqual(input, before);
assert.deepStrictEqual(first, second);
assert.notStrictEqual(first.ids, input);
console.log('Checks passed');
Run node prepare-record-ids.js. If a check fails, read the first differing field, locate the rule governing it, and follow one entry through your function by hand. For example, if duplicateCount is wrong, inspect whether you compared the original string or its trimmed form, and whether an invalid entry was counted twice. Avoid replacing the whole implementation when one branch is at fault.
The final checks cover two different promises. deepStrictEqual(first, second) checks repeatable result contents. Comparing input with before checks that the caller’s array was not changed. Neither check alone establishes both properties. After the supplied checks pass, review the written rules once more: a handful of passing cases is evidence, not permission to overlook an untested rule.
What you have built
You have translated a business contract into a JavaScript function with explicit input handling, ordered results, separate counts, and no external effects. More importantly, the implementation is yours: you can explain why each condition exists and which rule it satisfies. That ability is essential when AI assistance is unavailable—or when an AI-generated patch needs careful review.
In the next lesson, you will examine the time and space costs of a JavaScript solution. Keep your handwritten prepareRecordIds implementation: it gives you a concrete function whose behavior and resource use you can reason about separately.