AWS Builder Center
Building Straw2Cash's Frontend: A Mobile-First, Trilingual Marketplace for Rural India

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.ts
api.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'; // Punjabi
recognition.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:
  1. Total JS bundle < 300 KB gzipped. Achieved with dynamic imports and lazy-loading the map, chat, and admin sections.
  1. Skeletons, not spinners. Skeletons feel faster because they hint at what's coming.
  1. Optimistic UI where possible. Farmer listing appears immediately; sync happens in the background.
  1. Images: WebP, max 100 KB each.
  1. No auto-play video. Ever.
  1. 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.org
VITE_GO_API=http://localhost:8000
VITE_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  .... 
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article