Mid Fullstack Engineer Interview Questions
What a Mid Fullstack Engineer interview focuses on, the questions you'll face, and how to practice them with instant AI feedback.
What's expected at the Mid level
Expect independent end-to-end delivery and sound client/server trade-offs.
Sample Fullstack Engineer interview questions
- TechnicalHow do you decide what logic belongs on the client vs. the server?What a strong answer covers
- Security: sensitive operations must stay server-side
- Data freshness vs latency: real-time data often fetched from server
- Performance: heavy computation offloaded to server, UI updates client-side
- Bandwidth: minimize data transfer by processing on server
- User experience: optimistic updates on client, validation on server
View a sample answer
The decision hinges on security, performance, and user experience. Security is paramount: any operation that modifies data or accesses sensitive information must be on the server to prevent tampering. For data freshness, if the page needs live data (e.g., stock prices), the server should provide it via API or WebSocket; the client should only display and perhaps cache briefly. Performance wise, expensive computations (e.g., image processing, large sorting) should be server-side to avoid draining client resources, while UI interactions like drag-and-drop can be purely client-side. Bandwidth considerations: if the client can process raw data to reduce payload size, that's good, but sending too much data slows the app. A common pitfall is duplicating validation logic; always validate on the server even if you validate on the client. Follow-up: how do you handle state that needs to be synced? Use a state management library with server-driven updates.
- TechnicalWalk through how authentication and sessions work end to end.What a strong answer covers
- Password hashing with bcrypt/argon2 and secure storage
- Session token generation (JWT or random string) and HTTP-only cookies
- Session store (Redis, database) for persistent sessions
- Middleware verification on each request, with token refresh
- HTTPS to prevent token interception
View a sample answer
Authentication starts with the user submitting credentials (e.g., email and password) over HTTPS. The server hashes the password with a strong algorithm like bcrypt (salt + cost factor) and compares it to the stored hash. If valid, the server creates a session: either a random token stored in a session store (e.g., Redis with TTL) or a signed JWT containing user info and expiry. The session ID or JWT is set as an HTTP-only, Secure, SameSite=Strict cookie. On subsequent requests, the server’s middleware reads the cookie, looks up the session in the store (or verifies JWT signature), and attaches user data to the request. For sensitive operations, re-authentication may be required. A common pitfall is not rotating tokens or using short expiry; implement refresh tokens with rotation. On logout, delete the session from the store and clear the cookie. Follow-up: what about CSRF? Use anti-CSRF tokens or same-site cookies.
- TechnicalHow would you optimize a slow page that depends on a slow API?What a strong answer covers
- Measure and identify bottlenecks using browser DevTools and server logs
- Implement caching (CDN, server-side cache, service worker)
- Use lazy loading and code splitting for non-critical assets
- Optimize API calls: batch requests, paginate, use GraphQL or data-loader
- Fallback UI: skeleton screens, progressive enhancement
View a sample answer
First, profile the page to confirm the slow API is the main bottleneck (use Network tab and server-side tracing). Then, strategize: if the data is not highly dynamic, add a caching layer—CDN for static assets, server-side cache (Redis) for API responses, and a service worker for offline-first. For the API itself, consider batching multiple requests into one, adding pagination, or moving to GraphQL to fetch only needed fields. On the client, implement lazy loading: only call the API when the component is visible (Intersection Observer) and show a skeleton UI immediately. Use optimistic UI for actions that don't need immediate server validation. A common pitfall is caching stale data; set appropriate TTLs and invalidate on mutations. For extremely slow APIs, consider a fallback to stale data while the new response loads (stale-while-revalidate pattern). Follow-up: how do you handle API failures? Use retries with exponential backoff and a timeout.
- CodingBuild a small CRUD feature: schema, API endpoint and a UI form.What a strong answer covers
- Schema design: id, name, fields with appropriate types and constraints
- RESTful API: GET, POST, PUT, DELETE endpoints with validation
- UI form: input fields, submit button, list display with edit/delete
- Error handling and optimistic updates for better UX
View a sample answer
For a 'Todo' CRUD, I'd design a schema with columns: id (UUID), title (VARCHAR), completed (BOOLEAN default false), created_at (TIMESTAMP). The API would have endpoints: GET /todos (list), POST /todos (create), PUT /todos/:id (update), DELETE /todos/:id (delete). Use Express.js for the server, with input validation (e.g., title required). The UI is a simple HTML page with a form to add a todo, a list to display todos with edit and delete buttons. Use fetch() to call the API. Update the DOM dynamically. A common pitfall is not handling concurrent updates; use optimistic locking with timestamps. Below is a minimal but complete implementation.
Reference solutiontext -- SQL Schema CREATE TABLE todos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title VARCHAR(255) NOT NULL, completed BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT NOW() ); // API (Express.js) const express = require('express'); const { Pool } = require('pg'); const app = express(); app.use(express.json()); const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // GET /todos app.get('/todos', async (req, res) => { const result = await pool.query('SELECT * FROM todos ORDER BY created_at DESC'); res.json(result.rows); }); // POST /todos app.post('/todos', async (req, res) => { const { title } = req.body; if (!title) return res.status(400).json({ error: 'Title required' }); const result = await pool.query('INSERT INTO todos (title) VALUES ($1) RETURNING *', [title]); res.status(201).json(result.rows[0]); }); // PUT /todos/:id app.put('/todos/:id', async (req, res) => { const { id } = req.params; const { title, completed } = req.body; const result = await pool.query('UPDATE todos SET title = COALESCE($1, title), completed = COALESCE($2, completed), updated_at = NOW() WHERE id = $3 RETURNING *', [title, completed, id]); if (result.rows.length === 0) return res.status(404).json({ error: 'Not found' }); res.json(result.rows[0]); }); // DELETE /todos/:id app.delete('/todos/:id', async (req, res) => { const { id } = req.params; await pool.query('DELETE FROM todos WHERE id = $1', [id]); res.sendStatus(204); }); app.listen(3000); <!-- UI (HTML + JavaScript) --> <!DOCTYPE html> <html> <body> <h1>Todo List</h1> <form id="todo-form"> <input type="text" id="todo-input" placeholder="New todo" required> <button type="submit">Add</button> </form> <ul id="todo-list"></ul> <script> const API = '/todos'; const list = document.getElementById('todo-list'); const form = document.getElementById('todo-form'); const input = document.getElementById('todo-input'); async function loadTodos() { const res = await fetch(API); const todos = await res.json(); list.innerHTML = todos.map(t => `<li data-id="${t.id}"> <input type="checkbox" ${t.completed ? 'checked' : ''}> <span>${t.title}</span> <button class="edit">Edit</button> <button class="delete">Delete</button> </li>`).join(''); } form.addEventListener('submit', async (e) => { e.preventDefault(); const title = input.value; const res = await fetch(API, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({title}) }); if (res.ok) { input.value = ''; loadTodos(); } }); list.addEventListener('click', async (e) => { const li = e.target.closest('li'); if (!li) return; const id = li.dataset.id; if (e.target.classList.contains('delete')) { await fetch(`${API}/${id}`, { method: 'DELETE' }); loadTodos(); } else if (e.target.classList.contains('edit')) { const newTitle = prompt('New title:', li.querySelector('span').textContent); if (newTitle) { await fetch(`${API}/${id}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({title: newTitle}) }); loadTodos(); } } else if (e.target.type === 'checkbox') { await fetch(`${API}/${id}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({completed: e.target.checked}) }); loadTodos(); } }); loadTodos(); </script> </body> </html> - CodingImplement optimistic UI updates with rollback on failure.What a strong answer covers
- Immediately update UI state before server confirms
- Keep original state for rollback on failure
- Send request to server asynchronously
- On success, merge server response (e.g., ID) with optimistic state
- On failure, revert to original state and show error message
View a sample answer
Optimistic UI updates improve perceived performance by reflecting changes instantly. For example, in a React app with a 'like' button, when user clicks, I immediately increment the like count in local state and disable the button. I store the previous state in a variable. Then I call the API (e.g., POST /like). On success, I update the state with the server's response (e.g., the actual count) and enable the button. On failure, I rollback by restoring the previous count, re-enable the button, and show a toast error. A common pitfall is not handling multiple rapid clicks; use a mutation queue or disable the action until the server responds. Below is a simple implementation using vanilla JavaScript and fetch.
Reference solutionjavascript // Assume a UI component with state { likes: 10, liked: false, error: null } // Optimistic like function function handleLike() { const previousState = { likes: state.likes, liked: state.liked }; // Optimistic update state.likes += 1; state.liked = true; state.error = null; render(); // API call fetch('/api/like', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ postId }) }) .then(response => { if (!response.ok) throw new Error('Failed to like'); return response.json(); }) .then(data => { // Success: update with server response (e.g., actual likes count) state.likes = data.likes; state.liked = data.liked; render(); }) .catch(err => { // Rollback state.likes = previousState.likes; state.liked = previousState.liked; state.error = 'Failed to like. Please try again.'; render(); }); } // In React with hooks, you might use useReducer and handle rollback in the reducer. - System DesignDesign a commenting system with real-time updates and moderation.What a strong answer covers
- Real-time: WebSockets or Server-Sent Events for live updates
- Moderation: queue new comments for approval, flagging system
- Data model: comments table with parent_id for threading, status column
- Scalability: use event bus and worker for moderation tasks
- Edge cases: spam detection, rate limiting, notifications
View a sample answer
The system needs to support real-time commenting with moderation. Requirements: any user can post a comment; comments appear live for others after passing moderation; moderators can approve/reject/delete. I'd use WebSockets (socket.io) for real-time push. The data model: comments table with id, user_id, post_id, parent_id (nullable for threading), content, status (pending/approved/rejected), created_at. When a user submits a comment, the API inserts it with status 'pending' and publishes an event to a message queue (e.g., Redis Pub/Sub). A moderation worker picks up the event, applies auto-moderation (spam filter, profanity check), and if safe, updates status to 'approved' and broadcasts via WebSocket to all clients viewing that post. Moderators have a dashboard to manually review pending comments. Scaling: use multiple WebSocket servers with a shared session store and Redis adapter. A common pitfall is race conditions in moderation; use optimistic locking or versioning on the comments row. Follow-up: how to handle high traffic? Use a CDN for static assets, cache the comments list, and debounce real-time updates.
- BehavioralTell me about a feature you shipped entirely on your own.What a strong answer covers
- Situation: describe the problem and context
- Task: your specific responsibility
- Action: steps taken, technologies used, decisions made
- Result: quantitative or qualitative outcome
- Lesson: what you learned about ownership and planning
View a sample answer
At my previous job, we needed a bulk email sending feature for our marketing platform. The task fell to me because I had experience with async job queues. I designed the entire system: a React UI for composing and scheduling campaigns, a Node.js API endpoint that enqueues jobs to a RabbitMQ queue, and a worker service that processes emails using SendGrid. I also implemented retries with exponential backoff, tracking of delivery status in a database, and a real-time dashboard using WebSockets. The result: the feature shipped on time, handled 100,000 emails in the first hour without issues, and reduced our email sending cost by 30% compared to the previous manual process. I learned that owning a feature from end to end requires upfront architectural design, constant testing, and clear communication with stakeholders. The biggest challenge was error handling—ensuring no email is lost or duplicated. A follow-up question might be how I ensured idempotency.
- BehavioralHow do you decide which part of the stack to go deep on?What a strong answer covers
- Personal interest and strengths drive deep dive areas
- Project requirements may dictate where you need expertise
- Market trends and job opportunities influence focus
- Cross-functional learning: full-stack implies balance, not extreme depth in one
- Time allocation: 70% on current stack, 30% on adjacent technologies
View a sample answer
I decide based on a combination of project needs, team gaps, and my own curiosity. For example, when our team had performance issues on the frontend, I dove deep into React's reconciliation and context API to optimize rendering. Later, when we migrated to microservices, I invested time in learning Docker and Kubernetes. I also follow industry trends—if many companies are using a technology (e.g., TypeScript), I'll go deep on it. I believe in being 'T-shaped': broad enough to contribute across the stack, but deep enough in one or two areas (currently my depth is in frontend performance and backend APIs). I allocate about 70% of my learning time to the stack I currently use, and 30% to exploring new tools or paradigms. A common pitfall is going too deep too early; I start with a small project to validate interest before committing significant time. Follow-up: what's your current deep focus? I'm diving into event-driven architectures with Kafka.
What interviewers assess
Frontend craft
Component state, rendering and responsive, accessible UI.
Backend & APIs
Endpoint design, auth, validation and data modeling.
End-to-end thinking
Where to put logic, caching and the client/server boundary.
Databases
Schema design, queries and basic performance tuning.
Delivery
Testing, CI/CD and shipping safely behind flags.
How to prepare
- Pick a depth area and signal it clearly — generalists who go deep somewhere stand out.
- Narrate the full request lifecycle to show end-to-end understanding.
- Don't neglect testing and deployment; fullstack interviews probe delivery, not just code.
Frequently asked questions
Are fullstack interviews harder than specialized ones?
They are broader, not necessarily harder — you trade some depth for end-to-end coverage, but you still need real depth in at least one layer.
What should I focus on for a fullstack interview?
Be able to build a small feature across schema, API and UI, and explain client/server trade-offs clearly.
Do fullstack roles get asked system design?
Yes, usually pragmatic product-feature design that touches both frontend and backend rather than pure infrastructure.
Practice Fullstack Engineer questions with instant AI feedback
Offersly runs a mock interview tailored to your resume and target role, then scores every answer on relevance, depth, clarity and correctness.