Lessons from Building 8 Production Web Applications as a Solo Developer
Key lessons learned from building and maintaining 8 production web applications spanning Node.js, Python, Shopify, and e-commerce integrations.
The Portfolio
Over the past year, I built and deployed 8 production web applications: a WhatsApp booking system, an international shipping platform, a B2B wholesale portal, an AI image processing SaaS, a blog automation engine, a multi-channel inventory sync, a supplier stock dashboard, and a Shopify app. Here's what I learned.
Lesson 1: Start with the Database Schema
Every project that started with a solid database design went smoother than those where the schema evolved reactively. Spend the first day sketching your tables, relationships, and constraints.
-- This upfront planning saves weeks of refactoring later
CREATE TABLE bookings (
id SERIAL PRIMARY KEY,
customer_phone VARCHAR(20) NOT NULL,
visit_date DATE NOT NULL,
time_slot TIME NOT NULL,
purpose VARCHAR(50) NOT NULL,
is_blocking BOOLEAN DEFAULT true,
status VARCHAR(20) DEFAULT 'confirmed',
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT valid_status CHECK (status IN ('confirmed', 'cancelled', 'completed')),
CONSTRAINT unique_blocking_slot UNIQUE (visit_date, time_slot)
WHERE is_blocking = true AND status = 'confirmed'
);
The conditional unique constraint above prevents double-booking blocking slots. Getting this right at the schema level means your application code can be simpler.
Lesson 2: Railway Is Your Best Friend
I deployed all 8 applications on Railway. The total monthly cost across all projects: under $50. What would have been complex AWS infrastructure is a few clicks:
- PostgreSQL databases provisioned instantly
- Automatic HTTPS on custom domains
- GitHub push-to-deploy
- Environment variable management
- Logs and monitoring built in
For solo developers, managed platforms like Railway aren't just convenient — they're essential. Time spent on infrastructure is time not spent on features.
Lesson 3: External APIs Will Break
Every project integrates with at least one external API. They all have issues:
- WhatsApp Cloud API: Template approval takes days, rate limits are strict
- FedEx API: Sandbox is unreliable, production errors are cryptic
- Shopee API: Documentation has gaps, signature flow is complex
- Google Places API: Field masks are required but poorly documented
- Shopify API: Rate limits hit during bulk operations
The pattern: always wrap external APIs in a client class with retry logic, logging, and graceful degradation.
class ExternalApiClient {
async request(endpoint, options, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(endpoint, options);
if (response.ok) return response.json();
if (response.status === 429 && attempt < retries) {
await sleep(attempt * 2000); // Exponential backoff
continue;
}
throw new Error(`API error: ${response.status}`);
} catch (err) {
if (attempt === retries) throw err;
await sleep(attempt * 1000);
}
}
}
}
Lesson 4: Logging Saves Hours of Debugging
Structured logging from day one. Not console.log('something happened'), but:
console.log(JSON.stringify({
level: 'info',
event: 'booking_created',
bookingId: booking.id,
phone: booking.phone.slice(-4), // Last 4 digits only
date: booking.date,
duration: `${Date.now() - startTime}ms`
}));
When something breaks in production at 2 AM, structured logs tell you exactly what happened.
Lesson 5: Ship the Minimum, Iterate Fast
Pixel Prep launched with one feature: background removal. No batch processing, no marketplace presets, no credit system. Just upload an image, get it back without a background.
User feedback shaped everything that followed. Features I thought were essential (color backgrounds, shadow effects) weren't requested once. Features I didn't plan (batch ZIP upload, Shopee-specific sizing) were requested immediately.
Lesson 6: Automate What Hurts
Don't automate everything. Automate the things that cause actual pain:
- Inventory sync — overselling caused refunds and bad reviews
- Booking reminders — no-shows wasted staff time
- Blog generation — SEO requires consistent publishing
- Stock checking — 2 hours/day of manual work
Leave everything else manual until it becomes painful enough to justify automation.
Lesson 7: PostgreSQL Is Almost Always the Right Database
Across 8 projects with different requirements — booking slots, shipping documents, product catalogs, user accounts, sync states — PostgreSQL handled everything. Its JSONB columns cover the semi-structured cases, and its query capabilities eliminate the need for complex application-side data processing.
Lesson 8: Authentication Doesn't Need to Be Complex
For B2B portals and internal tools, simple JWT + httpOnly cookies work fine. OAuth2 is necessary for Shopify apps (because Shopify requires it) but overkill for a dealer portal with 30 users.
Lesson 9: Tests Save You When Refactoring
I won't pretend I had 90% coverage on every project. But the projects where I wrote tests for critical paths — payment processing, inventory calculations, booking conflict detection — were the ones where I could refactor confidently.
Minimum viable testing:
// Test the business logic, not the HTTP layer
describe('Booking Conflicts', () => {
test('rejects overlapping blocking bookings', async () => {
await createBooking({ date: '2026-04-15', time: '10:00', blocking: true });
await expect(
createBooking({ date: '2026-04-15', time: '10:00', blocking: true })
).rejects.toThrow('Slot already booked');
});
test('allows overlapping non-blocking bookings', async () => {
await createBooking({ date: '2026-04-15', time: '10:00', blocking: false });
const result = await createBooking({ date: '2026-04-15', time: '10:00', blocking: false });
expect(result.id).toBeDefined();
});
});
Lesson 10: Document Your Future Self
Six months from now, you won't remember why that FedEx API call has a specific timeout or why the Shopee token refresh runs every 3.5 hours. Comment the "why", not the "what":
// Shopee access tokens expire in 4 hours.
// Refresh at 3.5 hours to avoid race conditions
// during concurrent requests near expiry.
const TOKEN_REFRESH_INTERVAL = 3.5 * 60 * 60 * 1000;
The Bigger Picture
Building 8 production applications taught me that the technology matters less than the execution. Node.js or Python, React or vanilla JS, Prisma or raw SQL — these are implementation details. What matters is:
- Understanding the user's actual problem
- Shipping something that solves it quickly
- Making it reliable enough to run without constant attention
- Iterating based on real feedback
The best code is the code you don't have to think about because it just works.