https://snack-francisco.vercel.app/
Repo URL: https://github.com/joshkoshbgosh/SnackFrancisco
Provides a UI to search for food trucks in San Francisco using the official San Francisco Mobile Food Facility Permit dataset and utilizes the Google Maps API (Places Autocomplete and Distance Matrix) for location-based features and displaying trucks on a map.
Built with React (using TanStack Start), TypeScript, and shadcn/ui. It leverages Zod for runtime type safety and React Hook Form for managing form state.
Core Features Implemented:
- Search by Applicant Name: Users can type in a partial or full name of a food truck applicant. (case-insensitive)
- Search by Street Name: Users can type in a partial or full street name to find trucks located on that street. (case-insensitive)
- Filter by Status: Both applicant and street searches can be filtered by the food truck's permit status (e.g., APPROVED, REQUESTED, EXPIRED). The default status filter is "APPROVED".
- Proximity Search: Users can enter an address (via Google Places Autocomplete) or allow the application to use their current location (functionality for user's current location not explicitly built but address input is present). The application then displays food trucks sorted by distance.
- Interactive Map View: Food trucks are displayed as pins on a Google Map. Users can click on a truck in the list to highlight and center it on the map. The user's specified origin for proximity search is also marked.
- Framework (TanStack Start & Router):
- Chosen because all the cool kids are talking about it, and I wanted to see what the hype was about. Also chosen for data fetching/caching mechanisms, and type-safe full-stack capabilities (see
src/search/searchServerFn, a function that is only actually run on the server but the client can calls it as if it was running on the browser).
- Chosen because all the cool kids are talking about it, and I wanted to see what the hype was about. Also chosen for data fetching/caching mechanisms, and type-safe full-stack capabilities (see
- Styling (shadcn/ui + Radix / Tailwind):
- Love shadcn/ui's approach of providing ready-to-use components built on top of headless Radix UI components that you can adjust later because they're in your codebase, not a package.
- Schema Validation (Zod):
- Zod was used for defining data structures for API responses (food trucks, Google Distance Matrix), URL search parameters, and form inputs. This helped to ensure runtime type safety.
- Form Management (React Hook Form):
- Selected for its performance, ease of use, and seamless integration with Zod for validation and shadcn/ui's form wrapper components.
- Data Fetching & State:
- TanStack Router's loader capabilities, combined with server functions, were used for data fetching. This co-locates data dependencies with routes and provides built-in caching (
staleTime,gcTime).
- TanStack Router's loader capabilities, combined with server functions, were used for data fetching. This co-locates data dependencies with routes and provides built-in caching (
- API Interaction:
- Food truck data is fetched directly from the
data.sfgov.orgAPI. - Google Places Autocomplete API is used for address input.
- Google Distance Matrix API is used for calculating distances for proximity searches. API requests are batched to stay within usage limits.
- Food truck data is fetched directly from the
- Environment Variables (
@t3-oss/env-core):- Used to manage and validate environment variables (like API keys) for both client and server-side contexts, ensuring they are correctly loaded and typed.
- API Documentation (Swagger/OpenAPI):
- Implemented a public API
/api/searchand Swagger UI to interact with it. The app itself doesn't actually use the API, it's just a thin wrapper oversrc/lib/searchTrucks, which is also used by thesrc/server/searchServerFnmodule, which is what is actually used by the application on both the client and server.
- Implemented a public API
- More Comprehensive Testing: While unit tests for key utility functions and schemas were implemented, I would have added:
- Component tests using React Testing Library for UI components like
AddressAutocomplete(mocking external dependencies), the main search form, and the results display. - End-to-end tests (e.g., using Playwright or Cypress) to verify user flows.
- More extensive testing for the
searchTruckslogic, especially around edge cases with API responses.
- Component tests using React Testing Library for UI components like
- Update
AddressAutocompleteContract- All of shadcn/ui / radix's form control components comport themselves to react-hook-form's controller / context props for seamless integration with react-hook-form's state / error handling. I would like to do the same.
- Hide Coordinate Details from Users
- While I think encoding location in coordinates for the url is fine, user's ideally shouldn't see coordinates in the address input ever. This could be done on initial page load with coordinates in the query param by the client using google's geocoding features to do an address lookup and populate the address input. It could also be done on the backend to avoid the client making an extra request. For the case of when the user selects an address in the UI, the address text could remain in that input while a hidden input could store the coordinate string.
- Refined Error Handling & User Feedback:
- Implement more user-friendly error messages (e.g., toasts or inline messages) instead of
console.errorfor issues like failed address geocoding or form validation errors. - Provide clearer loading states for individual asynchronous operations beyond the global router loading state.
- Implement more user-friendly error messages (e.g., toasts or inline messages) instead of
- Performance Optimizations:
- For very large lists of trucks, implement virtualization for the results list (e.g., using TanStack Virtual).
- Further optimize re-renders in components.
- Accessibility (A11y): Conduct a more thorough accessibility audit and ensure all components are fully ARIA compliant. While Radix UI helps, custom interactions would need careful review.
- User's Current Location: Implement a button to allow users to use their browser's geolocation API to automatically populate the origin for proximity search.
- Debouncing Inputs: For search-as-you-type fields (applicant, street), add debouncing to API calls or filtering logic to improve performance and reduce unnecessary operations.
- Backend Data Caching: Although client-side caching was implemented, caching on the backend as well would be ideal
- Failed to show trucks with incomplete data Relied on SF's data portal for coordinate info, there were a few trucks that had human readable addresses in SF but their lat/lng coordinates were 0,0. For simplicity's sake, I just filtered them out, but I could have implemented address lookups for those cases.
- Simplicity of Distance Calculation: Relied on the Google Distance Matrix API for distance calculations. For a very high volume of requests, this could become costly. An alternative might involve Haversine formula calculations directly if only straight-line distance is needed and less accuracy is acceptable, or using a geospatial database (the socrata API provides opaque metadata that I believe map to zip codes / other geospatial data). Alternatively, since San Francisco isn't that big on a global scale, simple coordinate calculations assuming a flat earth could have worked.
- Full Data Loading: The entire food truck dataset is fetched at once. For significantly larger datasets, server-side pagination and filtering (e.g., via SoQL on the Socrata API if supported for all desired fields) would be ideal.
There are a number of // TODO comments in the codebase, i'll only touch on some of them here
- Full Test Suite: As mentioned, component and end-to-end tests were not fully implemented.
- Detailed Styling/Theming: Focused on functionality; UI could use some work.
- Type Safety for Extended FoodTruck Object: The
TODOcomment regarding the spread operator breaking type checking when addingdistance_metersandduration_texttoFoodTruckobjects was not fully addressed by creating a separateFoodTruckWithDistanceZod schema and type. This would be a recommended refactor for stricter type safety. Then the server function could return a union type that would allow the client to know when it's going to receive just the food truck data or with distances.
What are the problems with your implementation and how would you solve them if we had to scale the application to a large number of users?
- Data Fetching & Processing:
- Problem: Fetching the entire dataset from
data.sfgov.orgon every load and performing client-side/server-function filtering can be inefficient for many users or if the dataset grows significantly. - Solution:
- Dedicated Backend & Database: Implement a dedicated backend service that ingests the food truck data through periodic cron jobs into a database (e.g., PostgreSQL with PostGIS for geospatial queries, or Elasticsearch for text search).
- Server-Side Filtering/Pagination: The backend API would handle all filtering, sorting, and pagination, returning only the necessary data to the client.
- Geospatial Indexing: Use PostGIS or similar to perform efficient "nearest neighbor" searches directly in the database instead of calculating distances to all filtered trucks.
- Problem: Fetching the entire dataset from
- Logging / Observability
- Problem If a user ran into runtime errors or performance issues, I would have limited information.
- Solution Using a service like Sentry would help get visibility
- API Rate Limiting (External APIs):
- Problem: Heavy usage could lead to hitting rate limits for Google Maps APIs or the Socrata API.
- Solution: Backend Proxy & Caching: Route all external API calls through the dedicated backend. Implement caching (e.g., Redis) for responses from these APIs. Explore alternative approaches to distance calculation.
- Build Times / Size:
- Problem node_modules is already ~300MB in dev. Haven't looked into production bundle sizes
- Solution There are probably some dependencies in my package.json that are only needed for dev, I would look into that. Code splitting and tree shaking are other things I would look into.
- Security Risks
- Problem npm audit shows 13 moderate vulnerabilities that I haven't looked into. I also haven't set any limitations on my google maps api key
- Solution explore npm audit results, lock down api key capabilities / referrers
- Client-Side Performance:
- Problem: Rendering very large lists of trucks or complex map interactions could slow down the UI.
- Solution:
- List Virtualization: As mentioned, use libraries like TanStack Virtual for long lists.
- Map Marker Clustering: For dense map areas, implement marker clustering.
- Optimized State Management: Ensure efficient state updates to minimize re-renders. Didn't check for unecessary re-renders
- Replace Dynamic Map with Static Map The dynamic interactive google maps instance is cool but probably unecessary and could be replaced with google's static Map API for example.
- Node.js (v18 or later recommended)
- npm or yarn
- A Google Maps API Key with "Maps JavaScript API", "Places API", and "Distance Matrix API" enabled.
Create a .env file in the root of the project with your Google Maps API keys:
# Used by server-side logic (e.g., Distance Matrix API calls via server functions)
GOOGLE_MAPS_API_KEY=your_server_side_google_maps_api_key
# Used by client-side Google Maps (JS SDK, Places Autocomplete)
VITE_GOOGLE_MAPS_API_KEY=your_client_side_google_maps_api_keyNote: For security, ensure your client-side key has HTTP referrer restrictions and your server-side key has IP address restrictions if possible.
- Clone the repository:
git clone <repository-url> cd <project-directory>
- Install dependencies:
npm install # or # yarn install
npm run dev
# or
# yarn devThe application will be at http://localhost:3000
The public API will be at http://localhost:3000/api/search
The interactive Swagger API documentation will be at http://localhost:3000/docs
The OpenAPI JSON schema will be at http://localhost:3000/api/docs/swaggerJson
npm run build
# or
# yarn buildTo serve the production build locally (after building):
npm run start
# or
# yarn startUnit tests are implemented using Vitest.
npm run test
# or
# yarn test