Skip to content

Conversation

@ajaygandecha
Copy link
Member

@ajaygandecha ajaygandecha commented Aug 17, 2025

This pull request explores major changes to the way we fetch data in the CSXL app's frontend based on some lessons from the TypeScript world (from COMP 426) and some exploration I did over the summer with Dabblebase's stack (with a Python-based FastAPI server and Next.js / TypeScript frontend).

Rationale

One common pain point in the current fetching workflow between our Python-based backend and TypeScript-based frontend is the sharing of request / response types. These are defined in Pydantic models in the backend and TypeScript interfaces in the frontend. Any time one model is changed in the backend, the corresponding frontend model has to be changed.

Another common pain point that has arisen, especially as the app has grown in size (exhibit A: office hours feature) - we have some responses models that are shared between API endpoints, and custom Summary models returned by other endpoints. This can cause confusion when updating models and, I argue, is an unnecessary tangling of concerns.

Custom Request / Response Models Per Endpoint

To fix this immediate problem, I am thinking that we move away from having such groups of request / response Pydantic models and largely opt for defining separate models for each endpoint, one request (or more) and one response model. This allows for fine-tuning of data exposed from each endpoint helping to reduce data sent across the network to only what is necessary, ensure that changes to APIs remain isolated and do not accidentally affect other APIs, and provide easier naming (example, GetSampleRequest, GetSampleResponse, CreateSampleRequest and CreateSampleResponse instead of something like Sample, SampleDraft, SampleOverview).

Client-side OpenAPI Type Generation

In addition, in this PR, I explore OpenAPI type generation for generating frontend types rather than manually defining our own interfaces in frontend models. Since FastAPI conforms to the OpenAPI standard, our endpoint exposes an openapi.json file (https://csxl.unc.edu/openapi.json) that describes exactly the shape of the models that we use on the backend, including request types and response types! OpenAPI TS is a great package that allows us to generate TypeScript types from an OpenAPI file, giving us the ability to sync our frontend types to match our backend types.

This can be done with one command - when the development server is running:

npx openapi-typescript http://localhost:1560/openapi.json -o ./src/app/schema.d.ts

This creates a single file, schema.d.ts, which contains these types. This file can be found here.

Full, Type-Safe Fetching + Improved DX

This new, generated schema provides us great DX improvements. Since we are using TypeScript and our types are defined, we can create abstractions that greatly improve how we fetch data. Imagine something like this:

this.http.get("/api/sample");

where, as you type, the routes that match the GET method auto-completes:

Screenshot 2025-08-16 at 9 39 13 PM

where the returned data matches the type defined in the backend:

image

and where input parameters can be inputted directly into the HTTP client interface without any additional configuration will full type checking:

Screenshot 2025-08-16 at 9 42 50 PM

This is all possible - and demoed here. This PR includes an abstraction of Angular's HTTPClient, called OpenApiHTTPClient, that makes use of our generated OpenAPI schema to provide a full, type-safe API experience here

Using TanStack Query (FKA React Query) with Angular

To even further improve fetching, we could consider adding in TanStack Query for Angular - a derivative of TanStack Query for React (formerly known as React Query). TanStack Query has great docs on what it is about, but it includes two core fetching niceties - 1) providing accessible fetching states, including isLoading / isError etc, allowing for better frontend design to react to these states - 2) caching of requests made to a specific endpoint. There are also many other features that are included, including managing optimistic updating, pagination, etc. I will provide the docs here.

We use React Query in COMP 426, and it is become one of (if not the most) prevalent data-loading and caching libraries in the React world and is considered a must for large-scale projects there. I think that the Angular equivalent also provides some pretty nice features that we can use to improve UI and UX.

All Together

This PR contains an end-to-end example via the sample endpoint, accessible at /api/sample:

"""Sample API to show off OpenAPI integration"""
from fastapi import APIRouter
from ..models.sample import (
GetSampleItemsResponse,
GetSampleItemsResponse_Item,
GetSampleItemResponse,
CreateSampleItemRequest,
CreateSampleItemResponse,
)
api = APIRouter(prefix="/api/sample")
tag = "Sample Endpoint"
openapi_tags = {
"name": tag,
"description": "Sample API endpoint for showing off OpenAPI integration.",
}
@api.get("", tags=[tag])
def get_all() -> GetSampleItemsResponse:
return GetSampleItemsResponse(
items=[
GetSampleItemsResponse_Item(id=1, name="One", price=99.99),
GetSampleItemsResponse_Item(id=2, name="Two", price=199.99),
]
)
@api.get("/{id}", tags=[tag])
def get(id: int) -> GetSampleItemResponse:
return GetSampleItemResponse(id=1, name="One", price=99.99)
@api.post("", tags=[tag])
def create(_: CreateSampleItemRequest) -> CreateSampleItemResponse:
return CreateSampleItemResponse(id=1)

Types were generated using:

npx openapi-typescript http://localhost:1560/openapi.json -o ./src/app/schema.d.ts

Then on the frontend, we have the sample service:

@Injectable({
providedIn: 'root'
})
export class SampleService {
// Inject new, type-safe HTTPClient abstraction
private http = inject(OpenApiHHTPClient);
sampleService: any;
// This special example method wraps TanStack Query, providing back
// a reactive (signal-based) object with loading states and caching.
// See: https://tanstack.com/query/v5/docs/framework/angular/overview
queryGetAll() {
return this.http.queryGet('/api/sample');
}
getAll() {
return this.http.get('/api/sample');
}
get(id: number) {
return this.http.get('/api/sample/{id}', {
pathParams: {
id: id
}
});
}
create(request: components['schemas']['CreateSampleItemRequest']) {
return this.http.post('/api/sample', {
body: request
});
}
}

Our page uses the TanStack Query example, called here in the component TS file:

query = this.sampleService.queryGetAll();

Once this query is called, we can access it on the frontend HTML like so:

<div>
@if(query.isLoading()) {
<p>Loading...</p>
}
@if(query.error(); as error) {
<p>Error: {{ error.message }}</p>
}
@if(query.data(); as data) {
@for(item of data.items; track item.id) {
<p><strong>{{ item.name }}</strong> ({{ item.price }})</p>
} @empty {
<p>No items found.</p>
}
}
</div>

Notice how we have nice loading, error, and data states!

On the page itself, when we start loading for data, we get:

image

Once the data loads, we see:

Screenshot 2025-08-16 at 9 54 52 PM

Conclusion

While this would be quite a large refactor and likely adopted incrementally, I think we could get a great DX and UX improvement! This would potentially be great for the office hours feature.

Let me know your thoughts below! I am adding @jadekeegan for review here since this is an interesting connection to COMP 426.

@ajaygandecha ajaygandecha added the DONT MERGE This label is marked for pull requests that should not be merged yet. label Aug 17, 2025
@ajaygandecha ajaygandecha self-assigned this Aug 17, 2025
@ajaygandecha ajaygandecha changed the title [DO NOT MERGE] refactor: explore rethinking client-side fetching refactor: explore rethinking client-side fetching Aug 17, 2025
@ajaygandecha ajaygandecha mentioned this pull request Sep 19, 2025
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DONT MERGE This label is marked for pull requests that should not be merged yet.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants