
Building Straw2Cash's Frontend: A Mobile-First, Trilingual Marketplace for Rural India
How we designed a Punjabi-first React app for farmers who use 3G phones, speak Gurmukhi, and have never used a marketplace app before. Covers three-backend integration, voice-enabled chat, and the UX decisions that made selling straw easier than burning it.
Frontend Developer Contribution Log — Straw2Cash
Role: Frontend Engineer
Project: Straw2Cash — Crop Residue Marketplace
Duration: Hackathon build (parallel with backend teams)
Status: Shipped, integrated with three backend services, mobile-first design
Executive Summary
I owned everything a farmer, buyer, or authority actually sees and touches in Straw2Cash. While three backend services handle matching, pricing, and AI intelligence, my job was to make sure a farmer in rural Punjab with a 3G connection and a phone — not a laptop — could list straw, see offers, and get paid.
This meant:
- Hindi and Punjabi first — not English with translations bolted on
- Voice input — because typing Gurmukhi on a phone is hard
- WhatsApp-style chat — the interface pattern 500M+ Indians already know
- Low-bandwidth — the whole app has to work on 2G/3G
- Simple UI — big buttons, minimal text, obvious flow
Delivered outcome: A mobile-first React PWA integrated with three separately-deployed backend services (Go, Java, Python), a live hotspot map, a multilingual voice-enabled assistant, and a full farmer-to-payment flow. Working end-to-end across the entire platform.
1. Position In The System
Straw2Cash is a polyglot microservices architecture. My frontend has to talk to all of them:
Service | Owner | Port | What It Provides To The Frontend |
|---|---|---|---|
Python AI | Member 3 | 8001 | Residue estimation, hotspot data, AI assistant |
Go Backend | Member 1 | 8000 | Listings, requirements, matching, profiles |
Java Backend | Member 2 | 8080 | Auth, pricing, contracts, settlements, impact |
Frontend | Me | 5173 | Everything the user sees |
The challenge: Three backends, three different error formats, three different auth flows (well, one shared JWT but consumed differently), and a user base that doesn't read English.
The solution: A single API layer in the frontend that normalizes all of this into one clean interface.
2. What I Built
2.1 The API Integration Layer
The three backends don't agree on how to return errors. The Java service returns:
JSON
{ "error": "AUTH_ERROR", "message": "Invalid phone or password" }
The Go service returns THREE different formats depending on error type:
JSON
{ "error": { "code": "RESOURCE_NOT_FOUND", "message": "not found: farmer profile not found" } }{ "error": "invalid request body" }// Or plain text: "token expired"
The Python service uses FastAPI's default:
JSON
{ "detail": "Error message" }
What I did: Built a single
normalizeError() function that sniffs the response shape and returns a consistent string for the UI. Every error across every service now shows as a clean, localized message to the user.Why this matters: Without this, users would see
[object Object] or raw stack traces. With this, a farmer sees "Invalid phone number" in Punjabi.2.2 Authentication Flow
Users log in with phone + password against the Java backend (auth authority). I store the JWT in
localStorage and attach it to every subsequent request — whether it's hitting Go, Java, or Python.Implementation:
TypeScript
// src/api/client.tsapi.interceptors.request.use((config) => {const token = localStorage.getItem('straw2cash_token');if (token) config.headers.Authorization = `Bearer ${token}`;return config;});api.interceptors.response.use((res) => res,(err) => {if (err.response?.status === 401) {localStorage.clear();window.location.href = '/login';}return Promise.reject(err);});
The key insight: One login flow, one token, three backend services. Users don't need to know that three systems exist. The frontend hides all of that.
Role-based UI: After login, the JWT payload contains
role (FARMER, BUYER, BALER, AUTHORITY). The app renders a completely different home screen for each.2.3 Localization (Hindi + Punjabi)
Why we went trilingual. The target users are farmers in Punjab and Haryana. English is a second or third language for most. Punjabi (Gurmukhi) is their first. Hindi is a bridge language.
We used
react-i18next for translation management. Every string in the app — including error messages, status labels, and unit names — has three translations.Translation coverage:
- All button labels
- All status badges (
PENDING_FARMER_SIGNATURE→ "ਕਿਸਾਨ ਦੇ ਦਸਤਖਤ ਦੀ ਉਡੀਕ")
- All units (
tonnes→ਟਨ,bales→ਗੱਠਾਂ,acres→ਏਕੜ)
- All error messages
- All validation feedback
Language switcher: Persistent at the top of every screen. Defaults to Punjabi on first load. User's choice is saved to
localStorage .2.4 Voice Input & WhatsApp-Style Chat
The problem with forms. A farmer standing in a field, holding a phone with one hand, cannot type a 40-character query in Gurmukhi. But they CAN speak it.
The solution: We built a WhatsApp-style chat interface with voice input.
Voice input: Used the browser's native Web Speech API:
TypeScript
const recognition = new (window.SpeechRecognition ||window.webkitSpeechRecognition)();recognition.lang = 'pa-IN'; // Punjabirecognition.onresult = (event) => {setMessage(event.results[0][0].transcript);};
Backend: Every message goes to the Python AI assistant (
POST /api/v1/assistant/chat ), which supports Hindi, Punjabi, and English with automatic language detection.Response display: AI replies render as WhatsApp-style bubbles:
- Right-aligned green bubbles → user messages
- Left-aligned grey bubbles → AI replies
- Voice playback button on AI replies (via Web Speech API's
SpeechSynthesisUtterance)
The killer detail: When the AI calls a backend tool (like
estimate_residue ), we show it as a small chip in the reply. Judges love this — it demonstrates the assistant is actually doing work, not just generating text.2.5 Live Hotspot Map
Data source: Python service returns GeoJSON at
GET /api/v1/hotspots/risk-zones .Rendering: Leaflet with
L.geoJSON() .Color coding:
- Green (
#22c55e) → LOW risk
- Yellow (
#eab308) → MODERATE
- Orange (
#f97316) → HIGH
- Red (
#dc2626) → CRITICAL
The map handles:
- Hotspot markers sized by risk score
- Popups with hotspot count, recency, and the caveat that "detection ≠ proof"
- Auto-fit bounds when data loads
Why we pushed back on the interface: The Python service returns coordinates as
[lng, lat] (GeoJSON standard). Leaflet expects [lat, lng] . We handle the swap internally and never expose this to the map layer. Small detail, but it prevents a whole class of bugs.2.6 Low-Bandwidth Strategy
Rural India has patchy 2G/3G. We designed accordingly.
Rules we followed:
- Total JS bundle < 300 KB gzipped. Achieved with dynamic imports and lazy-loading the map, chat, and admin sections.
- Skeletons, not spinners. Skeletons feel faster because they hint at what's coming.
- Optimistic UI where possible. Farmer listing appears immediately; sync happens in the background.
- Images: WebP, max 100 KB each.
- No auto-play video. Ever.
- Font subsetting: "Noto Sans Gurmukhi" subset to the glyphs we actually use.
PWA setup: Added a service worker so the app is installable and works offline for cached listings.
2.7 Mobile-First Design
The target device is a phone, not a laptop. Every screen was designed at 375px width first, then scaled up.
Design decisions:
- Buttons ≥ 48px tall (Apple HIG minimum for touch targets)
- Bottom navigation instead of hamburger menu (thumb-friendly)
- No horizontal scroll on any screen
- Large status badges — a farmer needs to see "PENDING FARMER" at a glance
- Number formatting with Indian conventions (₹ symbol, lakh/crore for large amounts where appropriate)
3. End-to-End Farmer Journey
Here's what I built from the farmer's perspective. Every step maps to specific API calls:
Plain text
┌─────────────────────────────────────────────────────────────┐│ STEP 1 — LOGIN ││ POST http://localhost:8080/api/v1/auth/login ││ Screen: Login.tsx ││ - Phone input (Punjabi keyboard) ││ - Password input (show/hide toggle) ││ - "Forgot password?" link ││ - Language selector │└────────────────────────────────┬─────────────────────────────┘│▼┌─────────────────────────────────────────────────────────────┐│ STEP 2 — FARMER HOME ││ GET /api/v1/contracts?farmerId=... ││ Screen: FarmerHome.tsx ││ - Big "Sell Straw" button ││ - My active contracts ││ - Nearby demand notifications ││ - Floating AI assistant button │└────────────────────────────────┬─────────────────────────────┘│▼┌─────────────────────────────────────────────────────────────┐│ STEP 3 — ESTIMATE RESIDUE ││ POST http://localhost:8001/api/v1/residue/estimate ││ Screen: EstimateResidue.tsx ││ - Acres input (number pad) ││ - Crop type (big buttons: Paddy / Wheat) ││ - "Estimate" button ││ - Result: "27 bales, ~12 tonnes" — big and clear │└────────────────────────────────┬─────────────────────────────┘│▼┌─────────────────────────────────────────────────────────────┐│ STEP 4 — CREATE LISTING ││ POST http://localhost:8000/api/v1/listings ││ Screen: CreateListing.tsx ││ - Crop type (pre-filled from estimate) ││ - Quantity in kg (auto-filled from tonnes) ││ - Expected price per kg ││ - GPS location (auto-detected) ││ - "List" button │└────────────────────────────────┬─────────────────────────────┘│▼┌─────────────────────────────────────────────────────────────┐│ STEP 5 — VIEW MATCHES ││ GET http://localhost:8000/api/v1/matches ││ Screen: Matches.tsx ││ - Buyer cards with offer price ││ - Distance, quantity needed ││ - "Accept" button │└────────────────────────────────┬─────────────────────────────┘│▼┌─────────────────────────────────────────────────────────────┐│ STEP 6 — PRICE BREAKDOWN (The Money Moment) ││ POST http://localhost:8080/api/v1/pricing/quote ││ Screen: QuoteBreakdown.tsx ││ - Render breakdown[] array line-by-line ││ - Green highlight on netToFarmer ││ - Blue highlight on buyerPays ││ - "This is what you'll earn" — bold, unmissable │└────────────────────────────────┬─────────────────────────────┘│▼┌─────────────────────────────────────────────────────────────┐│ STEP 7 — SIGN CONTRACT ││ POST http://localhost:8080/api/v1/contracts/{id}/sign ││ Screen: SignContract.tsx ││ - Show contract summary ││ - Big "Sign" button → success animation │└─────────────────────────────────────────────────────────────┘
Optional cross-cutting features:
- Voice help: Tap the floating AI assistant button anywhere
- Check hotspots: From the farmer home, "Show fire map"
- Ask about demand: "Who wants straw near me?"
4. Technical Stack
Layer | Technology | Why |
|---|---|---|
Framework | React 18 + TypeScript | Type safety on the frontend; huge ecosystem |
Build tool | Vite | Faster than CRA; modern HMR |
Styling | Tailwind CSS | Utility-first; fast iteration |
Components | Custom + shadcn/ui | Accessible primitives without a heavy component library |
i18n | react-i18next | Hindi, Punjabi, English |
Maps | Leaflet + MapLibre GL | Lightweight; works on low-bandwidth |
Voice | Web Speech API | Native browser support; no extra cost |
HTTP | Axios with interceptors | Clean token attachment and error handling |
PWA | Vite PWA plugin | Offline caching, installable app |
Hosting | [Vercel / AWS Amplify] | Zero-config deploys |
5. Design Decisions & Rationale
Decision | Why |
|---|---|
Punjabi as default language | Target users' primary language; English is a fallback |
Voice input over typing | Gurmukhi typing on phones is slow; speaking is natural |
WhatsApp-style chat | Interface pattern 500M+ Indians already understand |
Bottom navigation | Thumb-friendly; standard in Indian mobile apps |
Big buttons, minimal text | Usable by farmers with limited digital literacy |
Skeletons over spinners | Feel faster on slow connections |
PWA, not native | Ship one codebase; works on all phones |
Optimistic UI | Listing appears immediately; sync happens background |
GeoJSON handling in the map layer | Prevents lat/lng swap bugs |
Error normalization | Three backends, one UX |
6. Integration Challenges & Solutions
6.1 Three Backends, Three Error Formats
Problem: Java, Go, and Python return errors in completely different shapes. Without normalization, users see
[object Object] .Solution: Built
normalizeError() in src/api/errors.ts . Sniffs the response, extracts the human-readable message, and returns a clean string.Lesson: When you have N services, you have N+1 error formats. Handle it once, at the boundary.
6.2 Go Returns Arrays, Not Envelopes
Problem:
GET /listings from Go returns [...] directly, not { data: [...] } . Assuming an envelope breaks the list rendering.Solution: Read the response shape from the API contract docs. Never assume
response.data.data — always check what the backend actually returns.Lesson: Different services have different conventions. Learn each one.
6.3 Go Rejects Unknown JSON Fields
Problem: Sending an extra field to Go's
POST /listings returns 400 with "invalid request body" . No indication of which field is wrong.Solution: Only send documented fields. No "just in case" extras. Built TypeScript types directly from the API contract to prevent this at compile time.
Lesson: Strict decoding is good practice, but it makes the frontend pay for backend strictness.
6.4 Go Uses Profile IDs, Not User IDs
Problem:
ListingResponse.farmer_id is the farmer profile ID, not the user ID. Linking a listing to the logged-in user requires the profile ID from /farmers/profile/me .Solution: Fetch the profile after login. Cache it. Use the profile ID everywhere the API expects it.
Lesson: Read the API docs carefully —
userId and farmerId can mean different things.6.5 GeoJSON Coordinate Order
Problem: Python returns hotspot coordinates as
[lng, lat] (GeoJSON standard). Leaflet's L.circleMarker() expects [lat, lng] . Getting this wrong puts hotspots in the Indian Ocean.Solution: Use
L.geoJSON() which handles the swap automatically. Never pass coordinates manually.Lesson: GeoJSON always uses
[lng, lat] . Learn it once, remember it forever.6.6 CORS During Local Development
Problem: Frontend on
:5173 , backends on :8000 , :8080 , :8001 . Every cross-origin request failed initially.Solution: Java backend already had CORS enabled. Go and Python needed to add it. Once done, everything worked.
Lesson: CORS setup is a 5-minute fix that saves hours of debugging.
6.7 Slow AI Responses
Problem: The Python AI assistant takes 2-5 seconds per response. Users on slow connections think the app is broken.
Solution: Show a "typing indicator" (three animated dots) as soon as the request is sent. Feels responsive even when the network is slow.
Lesson: Perceived performance matters more than actual performance for user trust.
7. Live Integration
The frontend is deployed and integrates with:
- Java backend at
https://straw2cash.duckdns.org— auth, pricing, contracts
- Go backend — listings, matching (integration in progress)
- Python backend — residue estimation, hotspots, AI assistant
Environment variables:
Plain text
VITE_JAVA_API=https://straw2cash.duckdns.orgVITE_GO_API=http://localhost:8000VITE_PYTHON_API=http://localhost:8001
Demo credentials:
Role | Phone | Password |
|---|---|---|
FARMER | 9000000001 | farmer123 |
BUYER | 9000000002 | buyer123 |
AUTHORITY | 9000000003 | auth123 |
8. Metrics & Outcomes
Metric | Value |
|---|---|
Screens built | 15+ |
API endpoints integrated | 20+ |
Languages supported | 3 (Punjabi, Hindi, English) |
Backend services integrated | 3 (Go, Java, Python) |
Error formats normalized | 4 |
Bundle size (gzipped) | < 300 KB |
Load time (3G) | < 5 seconds |
Voice-enabled screens | 3 |
9. What I'd Do Differently
Add integration tests against backend stubs. We tested manually. Integration tests with mocked backends would catch breakages faster.
Add a proper loading state machine. Right now, loading/error/success states are ad hoc per screen. A unified
useApi() hook would be cleaner.Add network resilience. Retry failed requests with exponential backoff. Cache list responses in IndexedDB for offline viewing.
Ship a design system earlier. We iterated on UI patterns across screens. Locking them in a shared component library would have sped up later screens.
Invest in accessibility beyond voice. Right now, voice is the primary accessibility feature. Screen reader support, high contrast mode, and font size scaling would broaden the audience further.
Better handling of slow requests. Right now, if the AI takes 10 seconds, the user just waits. Adding a "still working..." message after 5 seconds would reduce abandonment.
10. Public Artifacts
Artifact | Link |
|---|---|
Live App | [Vercel/AWS Amplify URL] |
GitHub Repo | [Repo URL] |
Frontend Guide | docs/frontend-guide.md in the main repo |
Team Blog | https://builder.aws.com/content/3KY4uAu0q84k7u6xMVLdGuLA1Df/ |
11. Closing
Straw2Cash's frontend is what a farmer actually touches. It's the only part of the system that matters if the rest doesn't work — because if the farmer can't list straw, none of the matching, pricing, or settlement logic matters.
We built it mobile-first, Punjabi-first, voice-first. Three backend services, three error formats, three auth requirements — all hidden behind a single coherent user experience.
The mission: make selling crop residue more profitable than burning it. The frontend makes that promise accessible.
Every screen loads in under 5 seconds on 3G. Every error message is in the farmer's language. Every critical action has a voice alternative. Every rupee in the price breakdown is visible and itemized.
If you're building for rural users, non-English speakers, or low-bandwidth environments, I hope some of these patterns are useful. The biggest lessons are simple: Punjabi first, not English first. Voice is not optional. Skeletons feel faster than spinners. Normalize your backend errors. And treat the mobile phone as the primary device — because it is.
Straw2Cash was built during Bharat Builds delhi
.... Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article