Overview
MockRelay is a developer tool for creating configurable HTTP endpoints without first building the backend that will eventually serve them. A developer can create a project, define a route such as GET /users/{id}, configure its status, headers, body and response delay, and immediately call the generated mock URL from an application or from MockRelay’s built-in test console.
I started the project while learning Spring Boot, but treated it as a complete product rather than a collection of disconnected course exercises. The implementation follows one request all the way through a React interface, a Spring Boot management API, PostgreSQL persistence and a dynamic mock runtime. The goal was to understand how those layers cooperate while building something I could genuinely use during frontend development.
An endpoint can now store multiple named response scenarios. For example, GET /users/{id} can preserve a successful response, a missing-user response and a slow response without requiring the developer to rewrite the endpoint each time. One scenario is active at a time, so the same stable mock URL can be used while testing different application states.
The problem
Frontend work often begins before the real API is stable, or before it exists at all. A happy-path JSON file is enough to draw the first screen, but it quickly stops being useful when the interface must also handle an empty result, a validation failure, a slow request, a missing resource or a response with custom headers.
At that point, developers commonly have to choose between hard-coded data inside the frontend, temporary server code that will later be discarded, or a general-purpose API client with enough configuration to interrupt the task they were originally trying to complete. Static sample APIs remove some setup, but their routes and behavior are not owned by the developer using them.
The problem I wanted to solve was therefore not simply “return some JSON.” It was:
How can a developer create an API-shaped dependency, control its important behavior, and test it from one focused workspace without building a temporary backend?
That framing established an important product boundary. MockRelay is not trying to replace an API client or provide another fixed public /users dataset. It is a small, editable mock server centered on the states a consuming application needs to experience.
The need for scenarios became clear while testing the first working version. An endpoint could already return 200, 404 or 500, but changing the status also meant overwriting its body, headers and delay. Returning to the previous state required recreating that configuration. Status codes described individual outcomes, but they did not preserve those outcomes for repeated use. That friction led to named response scenarios as the next product feature.
My approach
I designed the initial workflow around the shortest useful loop:
- Create a project with its own stable project key.
- Add an endpoint by choosing an HTTP method and path.
- Configure the response status, content type, headers, body and optional delay.
- Save additional named scenarios when the same route needs multiple outcomes.
- Activate the required scenario and copy the stable runtime URL.
- Send a real request from the built-in test console or an external client.
The workspace is deliberately dense and desktop-first. Its endpoint rail, editor and collapsible test console keep configuration and verification in the same view. The combined method and URL bar borrows a familiar interaction pattern from API clients, while the rest of the workspace is kept specific to configuring mock behavior rather than composing arbitrary requests.
I deliberately implemented a deterministic single-response version before adding scenarios. That gave me a working vertical slice and exposed the real overwrite problem through use. The scenario feature could then evolve an understood system instead of being designed speculatively. Conditional request matching and generated datasets remain outside the current scope because they introduce a different level of routing and rule complexity.
Technical decisions
Separate management and runtime flows. The React application uses /api/... routes to create and update configuration. Consumers call /mock/{projectKey}/... to receive the configured response. Keeping these flows separate prevents editor concerns from leaking into the public mock URL and makes their different responsibilities explicit.
Model the route separately from its possible behaviors. A MockEndpoint stores the HTTP method, normalized path and project relationship. Its MockResponseScenario records store the status, content type, headers, body and delay for each named outcome. The separation keeps route identity stable while response behavior changes independently.
Evolve the schema without discarding existing data. The original mock_responses table was renamed to mock_response_scenarios through Flyway. Existing rows became active scenarios named Default, which preserved the behavior of endpoints created before the feature. This was safer and more realistic than dropping the table and recreating development data.
Protect scenario rules in both the service and the database. Scenario names are unique per endpoint regardless of letter case, and a PostgreSQL partial unique index allows at most one active scenario for each endpoint. Activation runs inside a transaction: the current scenario is deactivated and flushed before the target becomes active. Service validation provides understandable errors, while database constraints remain the final defense against invalid state.
Treat route identity as more than a string. Paths are normalized before storage and parameter names are converted into a common signature. As a result, /users/{id} and /users/{userId} are recognized as the same route shape for a given method. At runtime, exact static routes take precedence over parameterized candidates.
Keep validation at both boundaries. The React editor gives immediate feedback for invalid JSON and conflicting routes, but the Spring API repeats the important validation because clients can bypass the interface. Validation failures and domain errors use a consistent RFC-style ProblemDetail response that the frontend API client can interpret.
Preserve HTTP semantics. Responses that cannot contain a body, such as 204, 205, 304 and informational statuses, skip body validation in the editor. The backend also clears or suppresses their body and Content-Type, so correctness does not depend on browser or servlet-container behavior.
Separate server state from unsaved editor state. TanStack Query owns scenarios and endpoints received from the backend. Local React state holds edits that have not been saved. Scenario switching, creation and activation are temporarily blocked when edits are pending, which prevents a selection change from silently discarding work or activating stale configuration.
Keep runtime behavior explicit. Only the active scenario is served by /mock/{projectKey}/.... Selecting an inactive scenario in the editor is an inspection and editing action, not an implicit runtime change. The developer must choose Set active, and the test console explains why sending is unavailable when the selected scenario is inactive.
Choose an intentional V1 trade-off for latency simulation. Configured delays currently block the request thread. This is straightforward to understand and sufficient for a personal V1 with limited concurrency. A scheduler or non-blocking runtime would be the appropriate refinement if concurrent load became a real requirement.
Outcome
The end-to-end workflow is now functional locally. Projects, endpoints and response scenarios are persisted in PostgreSQL. The editor can create, rename, update, activate and delete scenarios while keeping the mock URL unchanged. The runtime resolves both static and parameterized paths before returning the active scenario’s status, headers, body and delay. The test console makes a real request to that runtime rather than displaying a simulated frontend result.
The backend currently has 50 passing tests across controller, service, response-policy, domain-mapping and runtime behavior. Flyway owns the database schema, JPA auditing records creation and modification times, and Hibernate validates rather than silently changing the schema. Frontend linting and the production build pass, and the scenario workflow has also been exercised through the interface against the real Spring runtime.
MockRelay is still under active development. Authentication, public deployment and the product landing page are not presented as finished work. Named response scenarios are complete as a manually activated first version. Possible later milestones include request-selected scenarios, request logs, starter templates and generated sample data.
What I learned
The strongest lesson was that a small developer tool still needs clear system boundaries. A seemingly simple “save this JSON and return it later” feature touches UI state, request DTOs, validation, transactions, entity relationships, route resolution and HTTP semantics. Following one request across every layer made those relationships much easier to understand than studying each Spring feature independently.
I also learned that good developer experience depends on predictable details: preserving unsaved changes, showing useful validation near the field, distinguishing a timeout from an HTTP error, and returning the same error structure across the API. These are not decorative additions; they determine whether the tool feels trustworthy.
The scenario implementation reinforced the value of building a narrow vertical slice before generalizing it. Because the original single-response path already worked, I could identify which concepts needed to change and which should remain stable. The endpoint and mock URL stayed the same; the response became a collection with one active member. That made the migration smaller and kept the user workflow understandable.
If I were restarting the project, I would define the endpoint-response boundary and the error contract even earlier. Both decisions became foundations for the UI and for later features. My next challenge is to add more capability without losing the focused workflow that made the first version useful.
