Movie UI Dashboard is a responsive React dashboard for browsing, filtering, sorting, and maintaining movie records. The project was built to demonstrate practical trade-offs between REST and GraphQL data access while keeping the UI experience consistent across both API modes.
The dashboard includes movie search, genre filtering, date and revenue range filters, pagination, sorting, create/edit/delete workflows, theme switching, and an API diagnostics drawer for observing request behavior.
Movie UI Dashboard is the frontend layer of a larger movie platform system. It connects to REST and GraphQL APIs backed by a shared PostgreSQL data platform, allowing the same UI to compare different API access patterns without changing the user experience.
The frontend intentionally abstracts the transport layer so UI components, hooks, and feature workflows remain independent from whether data is delivered through REST or GraphQL.
This project was designed to explore how modern frontend applications can remain transport-agnostic while supporting multiple backend access patterns.
The goal was to demonstrate:
- reusable frontend architecture
- scalable server-state management
- REST vs GraphQL trade-offs
- predictable data-fetching patterns
- production-minded React architecture
βThe UI experience stays the same β the transport layer changes.β
The dashboard is built around several key frontend engineering principles:
- transport-agnostic UI architecture
- reusable feature-based organization
- scalable server-state management
- predictable query behavior
- separation of UI state and remote state
- reusable filtering workflows
- consistent API integration patterns
The goal is to keep the frontend flexible, maintainable, and independent from backend implementation details.
The dashboard intentionally separates UI workflows from transport implementation details. REST and GraphQL requests ultimately produce the same frontend data shape, allowing components, hooks, tables, dialogs, and filtering systems to remain reusable across both API modes.
The dashboard is designed around a transport-agnostic frontend architecture that can communicate with both REST and GraphQL APIs backed by a shared PostgreSQL platform.
sequenceDiagram
participant UI as React Dashboard
participant Query as TanStack Query
participant API as REST / GraphQL API
participant DB as PostgreSQL Platform
UI->>Query: Trigger movie query
alt REST Mode
Query->>API: Axios REST request
else GraphQL Mode
Query->>API: GraphQL request
end
API->>DB: Execute shared query logic
DB-->>API: Filtered movie data
API-->>Query: Response payload
Query-->>UI: Cached server state
| Area | Technologies |
|---|---|
| Frontend | React 19, TypeScript, Vite |
| Routing | TanStack Router with file-based route generation |
| Server state | TanStack Query |
| API clients | Axios for REST, graphql-request for GraphQL |
| UI state | Redux Toolkit, React hooks |
| Styling | Tailwind CSS 4, shadcn/ui, Radix UI, Geist font |
| Theme | next-themes light/dark mode |
| Forms and typed validation | React Hook Form resolvers, typed request payloads |
| Feedback | Sonner toast notifications |
| Icons | Lucide React |
| Testing | Vitest, Testing Library, jsdom |
| Tooling | ESLint, Prettier, TypeScript project references |
- Toggle between REST and GraphQL data sources from the same movie listing screen.
- Search movies by multiple search modes.
- Filter by genre, release date range, worldwide gross, domestic gross, and production budget.
- Sort and paginate server-backed movie results.
- Keep previous results visible while new filter requests are loading.
- Select GraphQL fields to control which columns and payload fields are requested.
- Create, update, and delete movie records.
- Show API request diagnostics for loading, placeholder, and refresh states.
- Support responsive layouts with light and dark themes.
src/
components/ Reusable app components and shadcn/ui primitives
config/ Runtime API target helpers
features/
apiMode/ Redux slice for REST/GraphQL mode
genres/ Genre API, types, and query hook
graphql/ Redux slice for selected GraphQL columns
movies/ Movie APIs, hooks, components, filters, types, tests
hooks/ Shared hooks
layouts/ App shell layout
lib/ API client, utilities, request diagnostics helpers
pages/ Page-level React components
routes/ TanStack Router file routes
store/ Redux store and selectors
test/ Vitest setup
main.tsx Application entry point
router.tsx TanStack Router instance
Modern frontend applications need predictable server-state management to handle caching, background refetching, pagination, filtering, and mutation synchronization without duplicating networking logic across components.
TanStack Query is used as the server-state layer for both REST and GraphQL. The root app creates a QueryClient in src/main.tsx and provides it through QueryClientProvider, so feature hooks can read cached data, trigger background refetches, and invalidate queries after mutations.
The main movie hook is useMovies in src/features/movies/hooks/useMovies.ts. It builds one query abstraction over two transport layers:
useQuery({
queryKey: ['movies', apiMode, filters, selectedFields],
queryFn: () =>
apiMode === 'graphql'
? getGraphQlMovies(filters, selectedFields)
: getMovies(filters),
placeholderData: keepPreviousData,
})This query key intentionally includes:
apiMode, so REST and GraphQL results are cached separately.filters, so search, sort, pagination, date, genre, and money range changes produce the correct server request.selectedFields, so GraphQL field selection changes create a new query result instead of reusing a payload with missing fields.
keepPreviousData keeps the table populated while new pages or filters are loading. That creates a smoother dashboard experience because the UI does not flash empty content between query changes.
Genres are loaded with useGenres in src/features/genres/hooks/useGenres.ts. Genre data is treated as long-lived reference data with staleTime: Infinity and gcTime: Infinity, which prevents unnecessary refetching for a mostly static list.
Mutations live in src/features/movies/hooks/useUpsertMovie.ts. Create, update, and delete actions use useMutation, then invalidate the shared ['movies'] query family on success:
await queryClient.invalidateQueries({ queryKey: ['movies'] })That keeps the listing synchronized after a movie record changes, regardless of the current page, filter, or API mode.
REST requests are implemented with Axios in src/features/movies/api/movies-api.ts and the shared Axios instance in src/lib/api.ts.
The shared client:
- Reads the base URL from
VITE_API_URL. - Sends JSON requests by default.
- Adds request metadata for API diagnostics.
- Tracks request start and finish timing.
- Shows normalized error messages through Sonner toasts.
Movie list filters are converted into query string parameters in src/features/movies/api/movie-query-params.ts. This keeps URL parameter construction separate from the React hook and makes the REST transport easier to test.
REST endpoints used by the movie feature:
GET /movies
POST /movies
PUT /movies/:id
DELETE /movies/:id
The REST path supports the full create, read, update, and delete workflow.
GraphQL requests are implemented in src/features/movies/api/movie-graphql-api.ts with graphql-request.
The GraphQL list query is built dynamically from the selected movie fields. The app always requests required identity/display fields such as id and movieName, then adds whichever optional fields are currently selected in the UI:
const columns = `
id movieName ${requestedFields.join(' ')}
`That selected field list is also part of the TanStack Query key, so changing visible GraphQL columns triggers a fresh request with the matching payload shape.
GraphQL filters are passed as variables instead of interpolated strings:
request(getGraphQlUrl(), query, {
filters: params,
})This keeps filter values separate from the query document and allows the API to receive a typed MovieFiltersInput.
GraphQL results are mapped back into the same MovieResponse shape used by REST. This is important because the table and dialog components can stay transport-agnostic:
items: data.movies.items.map(mapGraphqlMovie)GraphQL currently supports listing, updating, and deleting movies. Creating movies is intentionally guarded with an error message in the GraphQL API module because creation is only available in REST mode.
TanStack Router is configured in src/router.tsx:
export const router = createRouter({ routeTree })The app uses file-based routes from the src/routes directory. The TanStack Router Vite plugin in vite.config.ts generates src/routeTree.gen.ts, which is then passed into createRouter.
Current route files:
src/routes/__root.tsxdefines the root route and wraps every page withAppLayout.src/routes/index.tsdefines the/route and rendersMoviesPage.
The router type is registered globally with TanStack Router's module augmentation:
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}That gives route links, navigation, params, and router APIs strong TypeScript awareness across the app.
The project separates server state from UI state:
- TanStack Query owns remote data, caching, loading states, and mutation invalidation.
- Redux Toolkit stores cross-component UI preferences such as the selected API mode and selected GraphQL fields.
- Local React state manages page-specific filter controls before they become query filters.
Redux slices:
src/features/apiMode/apiModeSlice.tsstores whether the dashboard is using REST or GraphQL.src/features/graphql/graphqlColumnsSlice.tsstores selected GraphQL field keys and normalizes invalid values.
The movie page combines those slices with local filter state in src/pages/MoviesPage.tsx.
Movie filters are managed by useMovieFilterState in src/features/movies/hooks/useMovieFilterState.ts.
Search input, genres, date ranges, and money ranges are debounced before they are passed to TanStack Query. This prevents the app from making a server request for every keystroke or every intermediate range adjustment.
The debounced values are converted into a clean filter patch with buildDebouncedFilterPatch, then combined with pagination and sorting to form the final movieFilters object used by useMovies.
The UI is built with Tailwind CSS 4 and shadcn/ui-style primitives.
Key styling pieces:
src/index.cssimports Tailwind, shadcn styles, animation utilities, and the Geist variable font.- CSS custom properties define light and dark design tokens.
next-themestoggles thedarkclass on the document.- Reusable UI primitives live in
src/components/ui. - Feature-specific controls live beside the movie feature in
src/features/movies/components.
The layout is intentionally table-first and optimized for dashboard-style workflows. Filters, pagination, diagnostics, and mutation dialogs are designed to support repeated data-management workflows instead of a marketing-style page.
This dashboard requires one of the Movie API backends to be running:
- DotNetMovieApi
- NodeMovieApi
Both APIs connect to the shared Postgres Movie Platform and expose movie data through REST and GraphQL.
Default API URL:
VITE_API_URL=http://localhost:5000Create a .env file in the project root:
VITE_API_URL=http://localhost:5000
VITE_APP_PORT=3000VITE_API_URL is required for REST and GraphQL requests. The GraphQL client appends /graphql to this base URL.
VITE_APP_PORT is optional. If it is not set, Vite defaults to port 3000 in this project.
Install dependencies:
npm installStart the Vite dev server:
npm run devBuild for production:
npm run buildPreview the production build:
npm run previewRun tests once:
npm run test:runRun tests in watch mode:
npm testRun ESLint:
npm run lintFormat files with Prettier:
npm run formatCheck formatting:
npm run format:checkThe project uses Vitest with jsdom and Testing Library. Tests cover utility behavior, API query generation, GraphQL API mapping, page behavior, and movie dialog/listing interactions.
Examples include:
src/features/movies/api/movie-graphql-api.test.tssrc/features/movies/components/MovieListings.test.tsxsrc/features/movies/components/MovieUpsertDialog.test.tsxsrc/features/movies/hooks/useUpsertMovie.test.tsxsrc/features/movies/utils/buildMovieMutationPayload.test.tssrc/pages/MoviesPage.test.tsx
- A single movie hook hides the REST/GraphQL transport choice from the UI.
- Query keys include every input that can change the server response.
- GraphQL selected fields are stored globally so column visibility and payload shape stay aligned.
- REST query parameter construction is isolated in a utility function.
- Mutations invalidate the movie query family instead of manually patching every possible paginated/filter cache entry.
- Reference data such as genres is cached indefinitely.
- Styling uses theme tokens so light and dark modes share the same component structure.
This project emphasizes:
- transport-agnostic frontend architecture
- scalable server-state management
- reusable UI patterns
- predictable caching behavior
- centralized API abstractions
- typed frontend workflows
- responsive dashboard interactions
- separation of UI state and server state
The architecture is intentionally designed so the frontend can evolve independently from backend implementation details while maintaining a consistent user experience.
π§ DevAssist-AI
Production-minded Retrieval-Augmented Generation (RAG) application using OpenAI embeddings, semantic retrieval, and centralized AI orchestration workflows. Project
π¬ DotNetMovieApi
ASP.NET Core Minimal API implementation built on top of the Postgres Movie Platform, exposing shared PostgreSQL functions through REST and GraphQL endpoints. Project
β‘ NodeMovieApi
Node.js + TypeScript API implementation using Express and GraphQL Yoga, demonstrating reusable backend architecture and centralized data access patterns. Project
Centralized PostgreSQL data platform featuring Dockerized infrastructure, overloaded SQL functions, reusable query architecture, and shared business logic powering multiple APIs and applications. Project
- πΌ LinkedIn
βΆοΈ YouTube- π Portfolio
- π§ Email: stevenwickers@gmail.com
Steven Wickers Senior Frontend Engineer
React β’ TypeScript β’ Node.js β’ C# β’ PostgreSQL β’ Cloud
React + TypeScript movie dashboard built with Vite, TanStack Query, TanStack Router, Axios, Redux Toolkit, Tailwind CSS, shadcn/ui, REST, and GraphQL.








