This continues from Handling Validation and Error Display with Cotomy .
After validation and error display, the next practical screen type is the search screen. Search screens are often the entry point for business systems. They are where users start work, narrow down records, and choose the next screen.
This article focuses on standard list-style search screens, where results represent a shareable view rather than a transient UI state.
That does not make a search screen a mutation screen. It should not be designed like a save operation. Search is not a mutation path. It is a request to reconstruct a view from conditions.
That one distinction decides most of the implementation shape I usually choose for Cotomy screens. Search conditions should normally live in the URL query string in the kind of business systems I build. The server should normally construct the result list for ordinary list screens, especially when the screen handles many entities or when the page needs stable public visibility. Cotomy can still organize the query form and page lifecycle, but it should not turn the result list into unnecessary browser-owned state.
URL Query State
Search conditions are state. The question is where that state should live.
For ordinary search screens in CRUD-heavy business systems, the URL is the most stable owner. Reload works because the request contains the same conditions. Browser back works because each search can be represented as navigation. Bookmarks work because the address contains the condition set. Copy-paste links work because the receiver can open the same filtered view. Server logs also become easier to inspect because the request itself carries the condition values. This also improves operational traceability. Server logs, support investigations, and reproduction steps all benefit from query-driven requests.
Internal JavaScript state can be useful for small interaction details, but it should not become the primary source of truth for ordinary search screens. If the only real search condition state lives inside a client-side object, reload and link sharing become secondary features that must be rebuilt. For the kind of search screen this article targets, that is usually the wrong starting point.
Cotomy does not replace this model. It stabilizes the form behavior and page lifecycle so that query-driven screens follow the same operational structure as other screens.
Basic Shape
For this baseline, the recommended basic shape is direct. A Razor Page endpoint receives query parameters. The server loads matching records. The server renders the list. CotomyQueryForm standardizes the submit behavior and keeps the screen lifecycle consistent.
The form uses GET, and the query string represents the screen state.
The current CotomyQueryForm implementation fixes the form method to GET. On submit, it reads named inputs, merges them into the form action URL query string, removes empty values, and navigates by setting location.href. That behavior is useful for a query-driven screen because the result is still a normal page request.
Server-Rendered List
This example uses a simple sample entity list at /sample-entities. The screen has a keyword field, a category filter, and server-rendered result rows. The repository is intentionally small because the important part is the screen boundary.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
public class SampleEntitiesModel : PageModel
{
private readonly ISampleEntityRepository _repository;
public SampleEntitiesModel(ISampleEntityRepository repository)
{
_repository = repository;
}
[BindProperty(SupportsGet = true)]
public string Keyword { get; set; } = string.Empty;
[BindProperty(SupportsGet = true)]
public string Category { get; set; } = string.Empty;
public IReadOnlyList<SampleEntityListItem> Results { get; private set; } = [];
public async Task OnGetAsync()
{
Results = await _repository.SearchAsync(Keyword, Category);
}
}
public record SampleEntityListItem(string Id, string Name, string Category);
public interface ISampleEntityRepository
{
Task<IReadOnlyList<SampleEntityListItem>> SearchAsync(string keyword, string category);
}
The markup stays plain. The names of the inputs match the query-bound PageModel properties. When the request is /sample-entities?Keyword=invoice&Category=active, the server receives those values, loads the matching rows, and renders the list.
@page "/sample-entities"
@model SampleEntitiesModel
<h1>Sample Entities</h1>
<form id="sample-entity-search-form" method="get" action="/sample-entities">
<label for="keyword">Keyword</label>
<input id="keyword"
name="Keyword"
value="@Model.Keyword" />
<label for="category">Category</label>
<select id="category" name="Category">
<option value="">All</option>
<option value="active" selected="@(Model.Category == "active")">Active</option>
<option value="archived" selected="@(Model.Category == "archived")">Archived</option>
</select>
<button type="submit">Search</button>
</form>
<table>
<thead>
<tr>
<th>Name</th>
<th>Category</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Results)
{
<tr>
<td>
<a href="/sample-entities/@item.Id">@item.Name</a>
</td>
<td>@item.Category</td>
</tr>
}
</tbody>
</table>
There is no client-side result rendering in this baseline. The browser submits conditions as a query string. The server reconstructs the view. The rendered HTML is the result.
Paging, sorting, and complex condition preservation are important, but they add enough detail that they should be handled in a later Practical Guide article. This article keeps the baseline focused on the search screen itself.
CotomyQueryForm Role
CotomyQueryForm is not used to move search result state into the browser. It standardizes form behavior around a query-driven screen.
The TypeScript side can stay small. The page controller initializes the query form as part of the page lifecycle. The result list remains server-rendered.
import { CotomyElement, CotomyPageController, CotomyQueryForm } from "cotomy";
class SampleEntitySearchForm extends CotomyQueryForm {
}
CotomyPageController.set(class extends CotomyPageController {
protected override async initializeAsync(): Promise<void> {
await super.initializeAsync();
const form = CotomyElement.byId<SampleEntitySearchForm>(
"sample-entity-search-form",
SampleEntitySearchForm
);
if (!form) {
return;
}
this.setForm(form);
}
});
That is enough for the basic pattern. The form submit path is consistent with other Cotomy screens, but the result ownership does not move to TypeScript.
Sites, Blogs, And Catalogs
The same pattern also applies outside internal business systems. Blog index pages, documentation search or filter pages, product category pages, and public catalog pages often benefit from the same boundary.
When SEO matters, server-rendered results are usually the safer default. Search engines can see the content without waiting for a private client-side state transition. Preview tools and shared links receive meaningful HTML. Users can reload, copy links, and return with browser history in a predictable way.
The point is not that every search feature must be server-rendered. It is that ordinary index-like pages should start from stable URLs and server-rendered content unless there is a concrete requirement to move away from that shape.
A full page request may look less modern than partial updates, but that is not the main tradeoff I care about in this baseline. For standard list screens, the simpler request model often gives better reload behavior, easier debugging, clearer server logs, and fewer hidden state owners. When the workflow really needs partial updates, API-driven search can still be introduced deliberately.
When API-Driven Search Fits
This article describes the baseline I usually choose for Cotomy screens, not a universal rule for every web application.
For business systems that handle many entities, and for public pages where SEO, previews, links, and crawlability matter, URL query state and server-rendered results are usually the safer default.
There are also screens where API-driven search is the right choice. A private SPA, an autocomplete search, a dashboard with frequent partial refreshes, or a highly interactive grid may need AJAX-based search because the user is not simply reconstructing a list page from stable conditions.
The boundary is the important part. If the result is a shareable index page, start from the URL and server-rendered HTML. If the result is part of an application interaction, API-driven search can be a better fit.
Detail Navigation
Detail navigation depends on requirements. Cotomy should not force one detail navigation style. The list screen should provide stable links, and the application should choose the navigation model based on workflow, SEO, and operational needs.
In a business system list screen, clicking a row might navigate to /orders/{id} or /sample-entities/{id}. In a blog or documentation site, each result should normally link to the article URL. In a product or catalog site, each item should link to the product detail page. A modal or side panel detail can be valid when the list context must remain visible. A new tab or separate detail page can be valid when comparison or parallel work matters.
The baseline list should still expose clear links. That keeps the screen understandable even before richer interaction is added.
What I Avoid In This Baseline
There are a few choices I avoid when I am building this kind of search screen.
Using POST for ordinary searches hides condition state from the URL. Keeping conditions only in JavaScript state makes reload, history, bookmarks, and shared links less stable. Rendering SEO-sensitive lists only after client-side API calls makes the initial page less useful to crawlers and preview tools. Mixing server-rendered initial results with unrelated client-side result ownership creates two owners for the same list unless the boundary is explicit. Treating search as save-like mutation also pushes the screen toward the wrong lifecycle.
Those choices can be justified in specific application screens, especially SPA-like screens or highly interactive AJAX workflows, but they should not be the default for ordinary search and index pages.
Practical Boundary
The baseline boundary is simple. The server owns search result construction. The URL owns search conditions. Cotomy owns form behavior and screen lifecycle coordination. The application owns detail navigation requirements.
That division keeps the search screen stable. It also prevents Cotomy from being treated as a result-list state manager when the browser and server already provide a better request model.
Closing
This article defines the standard search-screen baseline I use with Cotomy. Search conditions should normally be URL query state when the screen is a business-system entry point or a public index-like page. Ordinary list results should normally be server-rendered when stability, SEO, link sharing, and browser navigation matter. CotomyQueryForm and CotomyPageController keep the screen lifecycle consistent without changing that ownership model.
From this baseline, the transition from search results to detail or edit screens can be handled more clearly. After that, edit screens without state drift, paging, sorting, and more complex condition handling can build on the same boundary.
Practical Guide
This article is part of the Cotomy Practical Guide, which focuses on hands-on usage patterns for the framework.
Series articles: Working with CotomyElement , CotomyPageController in Practice , Standardizing CotomyPageController for Shared Screen Flows , Building Business Screens with Cotomy , Handling Validation and Error Display with Cotomy , and Building Search Screens with Cotomy.
Next
Next article: a future Practical Guide article will cover the transition from search results to detail or edit screens.
Links
Previous: Handling Validation and Error Display with Cotomy . More posts: /posts/ .
Related Articles
CotomyForm in Practice , Form AJAX Standardization , Building Business Screens with Cotomy