
From 500 Errors to UX Wins: API Debugging in a Real-World Next.js 14 App
- Authors
- Name
- Stephen ♔ Ó Conchubhair
- Bluesky
- @stethewhitefox.bsky.social
🎢 Beyond the Basics: Overcoming Real-World API Challenges in a Next.js Survey App
Building a new feature or application can often feel like following a clear path, but the reality of development is rarely that straightforward. Recently, I embarked on a coding challenge to create a lightweight survey tool using Next.js 14 (App Router), TypeScript, and Tailwind CSS. The goal was simple: fetch surveys from an API, let users take them, and submit responses. What seemed like a straightforward task quickly became a masterclass in real-world API integration and persistent debugging. 👉 View the source code on GitHub
- 📋 Aligning with the Project Brief
- 🧠 Design Notes
- 🔧 Navigating Real-World Integration: A Journey Beyond Estimates
- Future Improvements
📋 Aligning with the Project Brief
The goal was to implement a lightweight survey tool that allows users to browse, take, and view surveys with thoughtful UX and best practices.
Important note: This wasn't a pass/fail test that will be scored out of correctness but rather an opportunity to demonstrate my ability to transform API data into a responsive and user-friendly UI.
🚀 Survey Application Development
This project involved the end to end development of a dynamic survey application, emphasizing a robust user experience and a modular, API-driven Architecture. Key accomplishments include:
Dynamic Survey Management: Designed and implemented a responsive "Survey List Page" that dynamically fetches and displays available surveys from the API, including titles and basic information.
API-Driven Form Rendering: Developed a flexible system to dynamically render survey questions based on API data, supporting various input types such as
single_choice
andmultiple_choice
.Comprehensive Submission & Validation: Engineering a robust submission process that collects user responses, transforms them to API's expected format, and sends them via
POST
requests. Integrating client-side validation using Zod schemas to ensure data integrity and provide immediate, clear error feedback.Modular & Maintainable Architecture: Designed with reusable components and a clear separation of concerns (e.g., API service, validation schemas, UI components) for enhanced maintainability and scalability.
Responsive UI: Styled with Tailwind CSS to ensure a seamless across devices.
API Resilience : Includes a fallback to mock data when the live API is unreachable, ensuring continuous development and testing workflows.
🛠 Tech Stack
- Next.js 14+ (App Router): Utilized for server-side rendering (SSR), intelligent routing, and efficient data fetching.
- TypeScript: Ensures strong typing throughout the codebase, improving code quality and reducing bugs.
- Tailwind CSS: A utility-first CSS framework for rapid and consistent styling.
- Zod: A powerful, TypeScript-first schema declaration and validation library.
- Workspace API: Provides survey data and accepts responses. Authentication and error handling are centralized in
lib/data.ts
. - Environment Variables: For secure API keys and base URLs.
- Tooling: Bun (optional) for faster installs and dev server.
🧠 Design Notes
- Next.js App Router for Performance & Security: Leveraging App Router to differentiate between Server Components (for fetching a list of surveys with links and details of each survey, preventing API tokens exposure) and Client Components (like the
SurveyForm
component, for interactive elements and form handling). This optimizes performance and security. - Modular API Layer (
app/lib/data.ts
): All interactions with theWorkspace
API are centralized here. This modularity ensures consistent error handling, authentication, and provides a clear separation of concerns, making the codebase easier to maintain and extend. Mock data fallbacks are included for development resilience. - Strongly Typed Forms with Zod: Client-side form submissions are rigorously validated using Zod schemas. This ensures that the data sent to the API conforms to expected types and formats, reducing invalid requests and providing immediate, clear feedback to the user, enhancing the overall UX.
- User-Centric Feedback: the UI provides distinct messages and visual cues for various submission outcomes: success, specific validation errors, API call failures, and general "Not Submitted" messages for unhandled issues.
🗂 Project Structure
survey-tool-app/
├── public/
├── src/
│ ├── app/
│ │ ├── components/ # Reusable UI components for the app directory
│ │ │ ├── QuestionField.tsx # Renders individual input fields based on question type
│ │ │ ├── ResponsesTable.tsx # Displays a table of survey responses
│ │ │ ├── SurveyForm.tsx # Client-side form component handling submission and validation
│ │ │ └── SurveysTable.tsx # Displays a table of available surveys
│ │ ├── lib/
│ │ │ ├── mock-data/ # Folder for mock API responses
│ │ │ │ ├── surveys.ts # Mock data for survey list
│ │ │ │ └── response-q1.ts # Mock data for individual question responses (example)
│ │ │ ├── data.ts # Centralized API calls, authentication headers
│ │ │ └── schemas.ts # Zod schemas for API response structures AND client-side form validation
│ │ ├── globals.css # Global styles for the application
│ │ ├── layout.tsx # Root layout for the application
│ │ ├── page.tsx # Home page (redirects to /surveys)
│ │ └── surveys/
│ │ ├── [surveyId]/ # Dynamic page for individual surveys (Server Component)
│ │ │ └── page.tsx
│ │ ├── responses/ # Folder for displaying survey responses/results
│ │ │ ├── [questionId]/ # Dynamic page for individual question responses
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx # Survey list page (Server Component)
├── .env.local # Environment variables (API keys, base URLs)
├── next.config.js # Next.js configuration (no rewrites needed as CORS is handled by API)
├── package.json # Project dependencies and scripts
├── tsconfig.json # TypeScript configuration
└── README.md # Project documentation
🎨 UX Considerations:
- Added loaders and error messages to guide the user.
- Used radio buttons for single-choice and checkboxes for multiple-choice questions for clear, guaranteed option values.
- Set up
baseURL
logic for consistent API calls in dev and during SSR.
✅ What's Working Well
- Response UI:
md:hidden
andmd:table
conditionally render mobile vs desktop views on the surveys page. - Dynamic Routing: Links to individual surveys and question results.
- Data Display: Clear listing of title, description, and question content.
- Accessibility:
aria-label
attributes and semantic clarity for screen readers.
🔧 Navigating Real-World Integration: A Journey Beyond Estimates
This project initially scoped for an "8-hour" completion, evolved into a comprehensive exercise in confronting real-world API challenges. This experience reinforced my framework-agnostic approach to application development, where the optimal tool is chosen for the specific task. I selected Next.js for its robust server side rendering (SSR) capabilities, which are crucial for secure data fetching and safeguarding sensitive credentials for dynamic user interactions.
- Proactive CORS Management:
Challenge & Approach: Anticipating potential browser-enforced Cross-Origin Resource (CORS) restrictions, I proactively implemented rewrites
in next.config.ts. This startegy proxies API requests, allowing the client-side application to seamlessly communcatire with the backend API without trigger CORS errors.
Outcome: While subsequent debugging with Swagger UI and curl
POST
requests, confirming that the API had no CORS restriction set up. Nevertheless, implementing rewrites
demonstrates a robust, proactive approach to API integration for scenarios where CORS is actively enforced.
- API Payload Transformation: The core problem was a clash between how the survey form gathered data and how the API expected to receive it. The API needed survey responses in a very specific JSON format: a list of objects, each containing a
question_id
and itsselected_option
.
For example, a single_choice
question would look like:
{
"responses": [{ "question_id": "q1_1", "selected_option": "Excellent" }]
}
And a multiple_choice
question:
{
"responses": [
{
"question_id": "q4_2",
"selected_option": "Dashboard, Notifications, Reports, Integrations"
}
]
}
However, the HTML form, specially the QuestionField
component was generating a flat FormData
object. This was due to incorrect name
attributes on the input elements. For instance:
- Radio buttons used
name={question.question}
instead ofname={question.id}
. - Checkboxes used
name={${question.id}[]}
whichFormData
interprets differently for multiple selections.
This mismatch meant the data sent to the backend was structurally wrong, leading to 500 Internal Server Errors
(which should have been a 400
series error for invalid input, but the data was so malformed it caused a server-side crash). Essentially, the backend couldn't understand the submitted survey answers.
The fix: Transforming the Data:
To solve this, a crucial data transformation step was added within the handleSubmit
function. This step acted as a translator, converting the form's FormData
into the API's required question_id
and selected_option
structure.
Here's how the solution worked:
Gathering Raw Data: The
handleSubmit
function first took theFormData
from the HTML form. It then processed these entries into a more usablerewData
object, correctly handling cases where multiple values where multiple values were associated with a single key (like formultiple_choice
questions).Mapping to API format: It then iterated through the
survey.questions
definition (which holds the expectedquestion.id
s). For each question, it looked up the corresponding answer in the rawData and transforming it into thequestion.id
andselected_option
format.For
multiple_choice
checkboxes, settingname={question.id}
, was key. This allowed theFormData
to group all selected options for a single question into an array, which could be then joined into a comma-separated string (e.g, "Dashboard, Notifications, Reports, Integrations") as expected by the API.Number inputs were also correctly converted to numbers.
The provided handleSubmit
TS code snippet is the core of the transformation, showing the logic for processing the raw form data and mapping it to the API's expected structure.
...
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
const form = e.currentTarget as HTMLFormElement
const formDataEntries = new FormData(form).entries()
const rawData: { [key: string]: string | string[] } = {}
const transformedResponses: PostResponseItem[] = []
for (const [key, value] of formDataEntries) {
if (rawData[key]) {
if (Array.isArray(rawData[key])) {
(rawData[key] as string[]).push(value as string)
} else {
rawData[key] = [rawData[key] as string, value as string]
}
} else {
rawData[key] = value as string
}
}
survey.questions.forEach(question => {
const rawAnswer = rawData[question.id]
if (rawAnswer !== undefined && rawAnswer !== '') {
let selectedOptionForApi: string | number | (string | number)[]
if (question.type === 'multiple_choice' && Array.isArray(rawAnswer)) {
selectedOptionForApi = rawAnswer.join(',')
} else if (question.type === 'number' && typeof rawAnswer === 'string' && !isNaN(Number(rawAnswer))) {
selectedOptionForApi = Number(rawAnswer)
} else {
selectedOptionForApi = rawAnswer as string
}
transformedResponses.push({
question_id: question.id,
selected_option: selectedOptionForApi
})
}
})
if (transformedResponses.length === 0) {
toast.error('Please select at least one question')
return
}
const loadingToastId = toast.loading('Submitting your survey...')
try {
const result = await postSurveyResponse(survey.id, transformedResponses)
if (result && (result.status === 'saved' || result.status === 'success_local_fallback')) {
toast.success('Survey Submitted Successfully!', { id: loadingToastId })
form.reset()
} else {
toast.error('Failed to submit survey. Please try again.', { id: loadingToastId })
}
} catch (error: unknown) {
console.error("Error during survey submission:", error)
}
}
...
View SurveyForm.tsx component code on GitHub.
- Client-Side Validation with Zod: To enforce data integrity and provide immediate user feedback, Zod was implemented for schema-driven validation, simplifying form state management and ensuring valid state management.
This validation acts as an early warning system, catching errors before they even reach the transformation step or the backend API, contributing to the more robust and user-friendly application.
- Server-Side Fetching and Absolute URLs:
Relative paths like /api/surveys
don't automatically resolve to http://localhost:3000/api/surveys
, leading to TypeError: Failed to parse URL
. To ensure consistency whether the fetch happened during SSR or on the client, I configured process.env.NEXT_PUBLIC_BASE_URL
dynamically provides the full absolute URL (e.g, http://localhost:3000
) for API calls originating from the server side.
- The Elusive 500 Internal Server Error on Submission: Despite correctly implementing CORS, payload structure, and absolute URLs,
POST
requests consistently resulted in a500 Internal Server Error
from the API, even though the exact request payload and headers worked via Swagger UI or curl. This suggests an unhandled exception or environmental quirk on the API's backend when processing a proxied request. In a team setting this would be escalated to the backend team with all debugging information. (Initial timeouts with Swagger also prompted robust mock data fallbacks.)
Task Breakdown:
- Survey List Page: Fetch and display a list of surveys from the API.
- View & Take a Survey: Click into a survey to view questions, providing a form for answering and submitting, dynamically rendering questions based on type (
single_choice
,multiple_choice
). - Submit Survey Response: Send the user responses to the API, handling successful requests gracefully.
- View Response Summary: After submission, show a simple summary of the submitted answers.
⚡ Setup and Running Locally
Clone the repository:
git clone https://https://github.com/theWhiteFox/survey-tool-app.git cd survey-tool-app
Install dependencies:
bun install # or npm install # or yarn install
Configure environment variables: Create a
.env.local
file in the root of the projectNEXT_PUBLIC_BASE_URL=https://interview.staging.company.com API_TOKEN=token
Run the development server:
bun dev # or npm run dev # or yarn dev
Running locally this is what you will see 🤞
Open http://localhost:3000 in your browser. Click the link to the surveys to see a list of available surveys.

Design Decisions & Best Practices
- Next.js App Router: Utilized for server-side rendering (SSR) of survey lists and detail pages, enhancing initial load performance and SEO (though not critical for this specific app, it demonstrates modern Next.js capabilities).
- Server Components & Client Components: API data fetching (
app/lib/data.ts
) primarily occurs in server-side functions (e.g., inpage.tsx
files) for better performance and security (API token is not exposed client-side). Interactive elements like theSurveyForm
are Client Components. - Modular API Service (
app/lib/data.ts
): Centralized API calls for reusability, maintainability, and consistent error handling/authentication. - Sensible State Management:
useState
for local component state (form data, submission status).loading
anderror
states are managed to provide clear user feedback.
- Responsive UI: Designed with Tailwind CSS to ensure a good experience across various screen sizes.
- User Experience (UX) Considerations:
- Clear titles and descriptions.
- Dynamic form elements (e.g., dropdown for
single_choice
questions to ensure valid input). - Immediate feedback after submission (success/error messages).
Future Improvements
- Multiple Choice Handling: Implement dedicated UI (e.g., checkboxes) and data collection logic for
multiple_choice
question types. - Response Summary Details: Enhance the "View Response Summary" page to provide more meaningful aggregated insights (e.g., breakdown of chosen options for each question).
- Form Validation: Implement more robust client-side validation (e.g.,
react-hook-form
or similar) beyond basicrequired
attributes. - Accessibility: Improve ARIA attributes and keyboard navigation.
- UI Polish: Further refine styling, transitions, and animations for a more polished user experience.
- Loading Skeletons: Implement skeleton loaders for better perceived performance during data fetching.
- Testing (Jest): Implementing unit and integrating testing using Jest.
- Managing state (Zustand): Explore Zustand for more complex global state management, if required, offering a simpler alternative to Redux.
- Storybook: Integrate Storybook for isolated UI component development and documentation.
⚠️ Lessons Learned & Reflection
Building this survey app was a miniature version of real-world frontend engineering. The requirement to interact with a strict API forced me to:
- Think deeply about data modeling and transformations.
- Troubleshoot ambiguous server errors methodically.
- Build resilient UX patterns that inform and guide users even when things go wrong.
- Leverage tools like Zod for validation and Next.js rewrites for elegant proxying.
Key takeaways
- Effective Next.js App Router Utilization: Strategically leveraged Server Components for efficient data fetching and improved security, and Client Components for interactivity and state management.
- Dynamic Component Rendering: Gained hands-on experience dynamically rendering UI based on API data, allowing for flexible and adaptable forms.
- Comprehensive Error Handling Strategies: Developed robust error handling for network issues, API failures, and validation errors, communicating clearly to the user.
- Complex API Data Transformation: Practical experience collecting raw form data from diverse inputs and accurately transforming it into the specific payload structure required by the backend API, particularly for complex question types like multiple-choice selections.
- API CORS Handling in Practice: Understood Next.js
rewrites
are truly necessary for CORS versus when the API itself is properly configured. - Building a Responsive UI with Tailwind CSS: Continuous learning in designing for different screen sizes and ensuring a consistent, accessible user experience.
This project was a valuable journey into building a robust and user-friendly frontend application integrated with a real API.
I wrote this blog post to reflect on what I learned during this project — including dynamic component rendering, Zod validation patterns, and managing typed form state and hosted on Vercel. Check out the Live Demo