A running results widget is a good first racing-data project because its essential behavior is easy to inspect. Load a small dataset, validate the fields you depend on, format durations clearly, and render a table that remains useful when information is missing. You do not need an account system or a live service to test those decisions.
This tutorial uses the fictional JSON bundled with RacingAPI.com. It is a static fixture, not an official race result and not a continuously updating endpoint. The interactive sample explorer loads the same family of local files so you can compare the raw records with the rendered view.
Start with a deliberately small contract
The demonstration contract has a schema_version, a sample flag, an event object, a classification_status, and a results array. Each result identifies an entry, a display name, a position, an elapsed duration in milliseconds, and a status. The downloadable schema documents the full fixture envelope.
The fixture is intentionally modest. It demonstrates a published snapshot rather than attempting to represent registration, payments, medical information, or an entire timing system. None of those unrelated features belongs in a first results widget.
Keep the sample flag in the record and show a sample label in the interface. Fictional data is useful precisely because it can be shared and changed without making statements about real participants. A polished design should not remove that important context.
Fetch the local file with an explicit error path
Serve the site through a standard static web server so its root-relative paths resolve correctly. The request below loads a bundled asset. It needs no API key and does not contact an external provider.
async function loadRunningFixture() {
const response = await fetch('/assets/data/running.json');
if (!response.ok) {
throw new Error(`Fixture request failed: ${response.status}`);
}
const data = await response.json();
if (data.sample !== true || !Array.isArray(data.results)) {
throw new Error('Unexpected fixture format');
}
return data;
}
The MDN guide to using Fetch explains an important detail: an HTTP error response does not necessarily reject the fetch promise. Check the response status explicitly, and handle JSON parsing or network failures in the calling code.
Do not use a catch block that replaces every failure with an empty results array. An unavailable file and a valid race with no published results are different states. Your interface should explain which one occurred.
Validate the fields you display
After checking the envelope, inspect each required result field. Confirm that identifiers and display names are strings, that a position is a positive integer or an explicitly allowed null, and that a duration is a finite non-negative number or a permitted missing value.
Do not rely on truthiness for numeric fields. Zero is a numeric value, while null means something different. Similarly, converting an unknown status string with a numeric function does not turn it into a valid duration.
Decide what to do with a malformed row. A small demo can show a clear error for the whole fixture. A production viewer might display unaffected rows and a warning, depending on its contract. Whatever you choose, avoid silently presenting a partially broken result as complete.
Format elapsed time without changing its meaning
Keep durations numeric until the rendering step. To display milliseconds as hours, minutes, seconds, and a three-digit fractional part, split the value using integer arithmetic after deciding how to handle precision. Do not parse a display string back into your calculation pipeline.
Treat a missing duration as unavailable, not as the fastest result. A clear em dash or a short status label is often appropriate. Keep the reason in another field when the source distinguishes not recorded, not applicable, and not yet published.
Label the timing basis. The fictional running fixture uses an illustrative elapsed duration. A production event may publish gun time, net time, or another specific value; your interface must identify which one it displays rather than calling every duration official time.
Preserve the supplied classification
The fixture already supplies position values. Render those values as part of its fictional classification instead of recalculating a ranking from the shortest available time. That preserves the distinction between displaying a source result and computing a new application view.
A local sort can still help organize the display, but name the behavior. Sorting by supplied position is not the same as deciding an official placing. Keep unclassified or unavailable positions separate rather than inventing a large arbitrary number and pretending it came from the source.
If you later add category views, retain the category context in the headings. An overall position and a category position are different fields. A single unlabeled number cannot reliably stand for both.
Render source text safely
Create table elements with the document API and assign participant labels through textContent. Avoid inserting source-provided names into innerHTML. A display name should be rendered as text, not interpreted as markup.
Keep the structural markup under your control. Use a real table with a caption, column headings, and an understandable empty-state message. Where a narrow screen needs horizontal scrolling, keep that scrolling within the table container rather than allowing the entire page to overflow.
Use readable type and numeric alignment. A results interface should remain comprehensible without color. If you use a colored status badge, include the status word and ensure that focus indicators remain visible for any interactive controls.
Show loading, success, empty, and failure states
A loading message tells users that the requested action is in progress. A success message should identify the loaded dataset rather than claiming a live connection. An empty state can say that the fixture contains no published results, while a failure state can explain that the sample could not be loaded.
Keep status announcements short and place them in an appropriate live region when JavaScript changes the content. Do not announce the entire result table every time a user switches examples. The page should support inspection, not overwhelm assistive technology with repetitive output.
The site includes direct links to the JSON files as well as the interactive loader. Those links remain useful without JavaScript and make the underlying data easy to examine. A progressive enhancement should add convenience without hiding the resource it demonstrates.
Prevent old requests from winning a race
When users switch between datasets, two requests can overlap. A slower earlier request may finish after the newer one. Without protection, it can replace the user's current selection with the wrong dataset.
Use a request counter or an abort mechanism so only the current selection updates the panel. Keep the selected button state and the loaded dataset consistent. Disable only the action that genuinely cannot be repeated, rather than making the entire page unresponsive.
This is a software race condition, separate from the sporting race represented by the records. A small fixture switcher is an excellent place to learn the distinction because the expected dataset is obvious and the failure is easy to reproduce with throttled networking.
Keep credentials out of the static prototype
The bundled files are public fixtures and require no secrets. Do not add an API-key field to the page or paste a private provider credential into its JavaScript. A static asset is downloadable by visitors, so it is not a safe place to conceal a server credential.
Connecting a private live source is separate backend integration work. It may involve a provider-approved public client flow or a server-side component, depending on the actual service. This tutorial does not invent that infrastructure or imply that changing one URL creates a secure production system.
Use the static prototype to settle the field contract, empty states, and rendering behavior first. Those decisions remain useful when an authorized delivery layer is introduced later.
Conclusion: make the simple path dependable
A good running results widget is explicit about its data source, timing basis, classification, and failure states. It validates the fields it uses, renders names as text, and does not turn missing values into misleading results.
Try the bundled example, inspect its data-model documentation, and test malformed or empty fixtures. Once the small widget behaves predictably, you have a concrete interface contract to discuss with a real event organizer or timing provider.



