This content originally appeared on DEV Community and was authored by Nico Hayes
The Beginning: When Concrete Meets Code
At 37, I was a civil engineer working on infrastructure projects – bridges, roads, and building foundations. My days were filled with CAD software, structural calculations, and construction site meetings. The most “programming” I’d done was writing Excel macros to automate load calculations.
But I had a side hobby: creating time-lapse videos of construction projects for social media. The problem? Every video needed background music, and licensing costs were eating into my tiny side project budget. Royalty-free sites offered either expensive tracks or generic music that made every construction video sound the same.
When AI music generation started making headlines, my engineering brain kicked in: “If AI can generate text and images, why not solve this music problem with code? How hard could building a simple web tool be?”
Six months later, that “simple tool” became musicgeneratorai.io – my first real web application that actually works and serves real users. This is the honest story of an engineer learning to code from scratch, including all the mistakes, late nights, and surprising parallels between building bridges and building software.
The Learning Phase: Engineering Mindset Meets Web Development
Coming from engineering, I approached learning to code the same way I’d approach any technical problem: systematically. I spent three weeks watching YouTube tutorials, treating “Build a Todo App” and “React for Beginners” like technical documentation. But programming felt different from engineering – more abstract, less visual.
In civil engineering, when you design a beam, you can see the load paths and understand the physics. With code, everything felt invisible and magical. Why did this JavaScript function work but that one didn’t? It was frustrating for someone used to concrete (literally) cause-and-effect relationships.
But my engineering background helped in unexpected ways. I was comfortable with problem decomposition, reading technical documentation, and iterative testing. While learning, I researched existing AI music solutions and discovered that several music generation APIs were available.
This was my “aha!” moment – just like in engineering, you don’t reinvent the wheel. You use proven components and focus on the novel integration. I didn’t need to understand machine learning; I needed to build a reliable interface around existing technology.
Technology Stack Decisions: An Engineer’s Approach
Choosing technologies reminded me of selecting materials for a construction project. In civil engineering, you don’t pick the fanciest steel; you pick what’s proven, well-documented, and fits your load requirements. I applied the same logic to web development.
I needed tools that were:
- Well-documented (like engineering standards)
- Widely adopted (proven in production)
- Beginner-friendly (I was definitely a beginner)
- Suitable for my specific “load” requirements
Frontend: Next.js 15 + TypeScript
I chose Next.js because it reminded me of integrated engineering software – one tool that handles multiple functions instead of juggling separate programs. In engineering, we use software like AutoCAD that combines drafting, calculation, and documentation. Next.js promised the same integration for web development.
TypeScript felt familiar coming from engineering software with strict parameter validation. In structural analysis, if you input the wrong unit or data type, the software catches it immediately. TypeScript does the same for code – it prevents the web equivalent of using PSI when you meant MPa. The early error catching was worth the learning curve.
Database: PostgreSQL + Drizzle ORM
Database design felt familiar – it’s like creating the foundation and structural framework for a building. You need solid fundamentals that can handle the loads you’re planning for. PostgreSQL is the reinforced concrete of databases: reliable, proven, and can handle whatever you throw at it.
I chose Drizzle ORM because it reminded me of engineering calculation sheets – everything is explicit and traceable. You can see exactly what SQL queries are being generated, just like you can trace through structural calculations step by step.
Here’s how my database schema evolved:
// Music generation tracking
export const musicGenerations = pgTable('music_generations', {
id: bigserial('id', { mode: 'number' }).primaryKey(),
uuid: uuid('uuid').$defaultFn(() => uuidv4()).notNull().unique(),
user_uuid: uuid('user_uuid').notNull().references(() => users.uuid),
// Generation parameters
prompt: text('prompt').notNull(),
generateMode: musicModeEnum('generate_mode').default('custom').notNull(),
duration: real('duration').default(30).notNull(),
credits_consumed: integer('credits_consumed').notNull(),
// Status tracking
status: musicGenerationStatusEnum('status').default('pending').notNull(),
progress: integer('progress').default(0).notNull(),
// Timestamps
created_at: timestamp('created_at').defaultNow().notNull(),
updated_at: timestamp('updated_at').defaultNow().notNull(),
})
Authentication: Google OAuth
Building user authentication from scratch sounded like a nightmare for someone who barely understood how websites worked. I spent two days reading about password hashing, session management, and security vulnerabilities before deciding to use Google OAuth.
Google OAuth was perfect for a beginner because:
- Users already have Google accounts
- No password management headaches
- Google handles all the security
- One less thing for users to remember
The implementation was simpler than I expected – mostly copying code from documentation and tutorials. Most users just click “Sign in with Google” and they’re ready to create music.
UI Framework: Tailwind CSS + Radix UI
I used Tailwind CSS because I was already familiar with it from tutorials. Radix UI components provided accessible, customizable building blocks that saved enormous amounts of time. The combination let me create a professional-looking interface without deep design expertise.
File Storage: Cloudflare R2
Initially, I tried storing generated music files directly on my hosting server, which was a disaster. The first few users filled up my storage, and large files made the site incredibly slow.
After researching options, I chose Cloudflare R2 because:
- Much cheaper than AWS S3 (no egress fees)
- Simple API that worked like S3
- Global CDN for fast downloads
- Perfect for a cash-strapped beginner
Setting up file uploads and downloads took me a week of debugging, but now users can generate and download music files reliably without breaking my budget.
Building the Five Generation Modes
The platform supports five different ways to create music, each requiring different technical approaches:
1. Song from Inspiration
This became the most popular feature – users simply describe what they want: “upbeat electronic music for a tech product demo.” The challenge was translating natural language into effective AI prompts.
I built a prompt enhancement system that analyzes user input and adds technical parameters:
export class MusicGenerationService extends BaseService {
static async createGeneration(params: GenerationParams): Promise<{
generationId: string
estimatedDuration: number
creditsUsed: number
}> {
const currentUser = await requireUser()
// Validate parameters and check credit balance
this.validateGenerationParams(params)
const requiredCredits = this.calculateRequiredCredits(params)
const balance = await CreditAccountService.getUserBalance(currentUser.uuid)
if (balance.current_balance < requiredCredits) {
throw new BusinessError('Insufficient credits', ERROR_CODES.INSUFFICIENT_CREDITS)
}
// Create generation record and process
const generation = await this.createGenerationRecord(currentUser, params, requiredCredits)
return this.processGeneration(generation)
}
}
2. Custom Music Creation
For users wanting detailed control, this mode provides parameters like tempo, key signature, instruments, and song structure. The interface uses progressive disclosure – basic options are prominent, while advanced controls are expandable.
3. Lyrics to Song
This feature analyzes text input to create complete songs with melody and instrumentation. The technical challenge was parsing lyrical structure and mapping it to musical arrangements.
4. Instrumental Mode
Pure instrumental tracks for background music. These generations focus on mood and atmosphere rather than complex vocal arrangements.
5. Cover Song Generation
Users can upload existing songs for AI-powered style transfers. This required additional audio processing capabilities and careful handling of copyright considerations.
Technical Architecture Deep Dive
The Generation Pipeline
Managing the music generation workflow was one of the biggest technical challenges. The process involves multiple stages, each with potential failure points:
- Request Validation: Checking user permissions, credit balance, and parameter validity
- Credit Reservation: Temporarily freezing credits to prevent double-spending
- API Integration: Sending requests to AI music generation services
- Progress Tracking: Providing real-time updates to users
- File Processing: Downloading, optimizing, and storing generated audio
- Completion: Finalizing the generation and updating user credits
Database Design Evolution
My initial schema was naive – just a simple songs
table. As features grew, I realized I needed to track:
- User credit systems for monetization
- Generation metadata for debugging
- File storage locations and expiration
- User preferences and favorites
- Analytics for understanding usage patterns
The current schema separates concerns clearly:
// Separate tables for different aspects
export const musicGenerations = pgTable('music_generations', {
// Generation request and status
})
export const musicTracks = pgTable('music_tracks', {
// Individual song results
generation_uuid: uuid('generation_uuid').references(() => musicGenerations.uuid),
clip_id: varchar('clip_id', { length: 128 }).notNull(),
audio_url: text('audio_url'),
r2_audio_url: text('r2_audio_url'), // Our CDN copy
play_count: integer('play_count').default(0),
download_count: integer('download_count').default(0),
})
Credit System Implementation
Implementing a reliable credit system was crucial for monetization. The system needed to handle edge cases like failed generations, refunds, and concurrent requests:
export class CreditTransactionService extends BaseService {
static async createTransaction(params: {
userUuid: string
amount: number
type: CreditTransactionType
description: string
referenceId?: string
}) {
return TransactionManager.executeTransaction(async (tx) => {
// Atomic credit updates with transaction safety
const account = await CreditAccountService.getUserAccount(params.userUuid, tx)
const newBalance = account.current_balance + params.amount
if (newBalance < 0) {
throw new BusinessError('Insufficient credits')
}
// Update balance and log transaction atomically
await Promise.all([
CreditAccountService.updateBalance(params.userUuid, newBalance, tx),
this.logTransaction(params, tx)
])
return newBalance
})
}
}
Real-Time Progress Updates
Users needed feedback during the 30-60 second generation process. I implemented Server-Sent Events for real-time progress tracking:
// API endpoint for progress streaming
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const generationId = searchParams.get('id')
const encoder = new TextEncoder()
const stream = new ReadableStream({
start(controller) {
const sendUpdate = (progress: number, message: string) => {
const data = `data: ${JSON.stringify({ progress, message })}\n\n`
controller.enqueue(encoder.encode(data))
}
// Subscribe to generation progress events
subscribeToGeneration(generationId, sendUpdate)
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
}
})
}
User Interface Design Decisions
Simplicity Over Features
My initial interface was overwhelming – tempo sliders, genre dropdowns, instrument selectors everywhere. User testing revealed that most people just wanted to describe what they needed in plain English.
The current interface is deliberately minimal: a large text input with the prompt “Describe the music you need” and a generate button. Advanced options are available but hidden by default.
Component Architecture
I used a modular component approach with clear separation of concerns:
export default function CreateToolClient({ tool, locale }: Props) {
const { user } = useAuth()
const { creditBalance } = useCredits()
const [selectedTrack, setSelectedTrack] = useState<ClientMusicTrack | null>(null)
// Tool-specific content rendering
return (
<CreateLayout>
<ToolContent
tool={tool}
onTrackSelect={setSelectedTrack}
creditBalance={creditBalance}
/>
</CreateLayout>
)
}
Mobile-First Design
About 40% of users access the platform via mobile devices. The interface needed to work seamlessly across screen sizes, which influenced every design decision from button sizing to form layouts.
Payment Integration Challenges
Adding payments was the most stressful part of development. Stripe’s documentation is excellent, but implementing subscription billing with credits, upgrades, and cancellations required understanding many edge cases.
My first implementation had bugs – users occasionally got charged twice or didn’t receive credits. These issues taught me the importance of thorough testing with payment systems and proper webhook handling:
// Stripe webhook handling with proper verification
export async function POST(request: Request) {
const body = await request.text()
const signature = request.headers.get('stripe-signature')
try {
const event = stripe.webhooks.constructEvent(body, signature!, webhookSecret)
switch (event.type) {
case 'invoice.payment_succeeded':
await handleSuccessfulPayment(event.data.object)
break
case 'customer.subscription.deleted':
await handleSubscriptionCancellation(event.data.object)
break
}
return Response.json({ received: true })
} catch (err) {
logger.error('Webhook signature verification failed', err)
return Response.json({ error: 'Invalid signature' }, { status: 400 })
}
}
Scaling Challenges and Solutions
The Database Performance Wall
As usage grew, certain database queries became unacceptably slow. The generations table quickly reached hundreds of thousands of rows, and user dashboard queries were taking multiple seconds.
I learned about database indexing the hard way:
-- Strategic indexes for common query patterns
CREATE INDEX idx_music_generations_user_status
ON music_generations(user_uuid, status);
CREATE INDEX idx_music_generations_created_at
ON music_generations(created_at DESC);
CREATE INDEX idx_music_tracks_generation
ON music_tracks(generation_uuid);
File Storage Cost Management
Storing thousands of audio files became expensive quickly. I implemented intelligent lifecycle management:
- Files automatically expire after 30 days for free users
- Paid users get permanent storage
- Popular tracks are cached on CDN
- Unused files are automatically cleaned up
API Rate Limiting
The AI music generation service had strict rate limits. I built a queue system with priority levels – paid users get faster processing, while free users wait longer but still get their music.
Monitoring and Analytics
Understanding user behavior was crucial for product development. I integrated comprehensive analytics:
// Custom analytics service
export class AnalyticsService extends BaseService {
static async trackGeneration(params: {
userId: string
generationType: string
duration: number
success: boolean
}) {
// Track both business metrics and technical performance
await Promise.all([
this.logUserEvent(params),
this.updateUsageMetrics(params),
this.recordPerformanceData(params)
])
}
}
Key metrics I track:
- Generation success/failure rates
- User retention and conversion
- API response times and errors
- Credit usage patterns
- Popular music styles and prompts
Business Model Evolution
Finding the Right Freemium Balance
Pricing was guesswork based on competitor research and my own budget constraints. I studied the pricing strategy document I created and landed on:
- Free: 10 credits daily (2 songs), basic features
- Basic ($12.99/month): 400 credits monthly, faster generation
- Standard ($24.99/month): 1000 credits monthly, commercial license, high quality
- Pro ($49.99/month): 2000 credits monthly, API access, priority support
The credit system (5 credits per song) gives users flexibility while managing my API costs. Most users stay on the free tier, but the few who upgrade help cover expenses.
User Behavior Insights
Six months of data revealed fascinating patterns:
- 70% of users only use the simple “inspiration” mode
- Users typically generate 3-4 variations before settling on a track
- Weekend usage spikes 300% (hobby creators working on personal projects)
- Commercial licensing drives most conversions to paid plans
- Users who save their first generation have 4x higher retention
Current State: The Humbling Reality
Let me be completely honest about where things stand after six months:
User Growth: ~200 monthly active users (not thousands, just a couple hundred real people)
Revenue: $380/month (barely covers hosting and API costs)
Technical Performance: 98% uptime (occasional crashes when I deploy bugs)
Conversion Rate: 8% from free to paid (a few users actually pay!)
These aren’t impressive numbers, but for someone who couldn’t code six months ago, seeing real people use something I built feels incredible. Every paid user feels like a small miracle.
Mistakes Made and Lessons Learned
Technical Mistakes
Over-Engineering Early Features: I spent weeks building an advanced audio editor that <5% of users ever touched. Focus on core functionality first.
Inadequate Error Handling: My initial error messages were terrible. “Generation failed” tells users nothing useful. Better error messages improved user satisfaction significantly.
Ignoring Mobile Users: Launched desktop-only when 40% of traffic was mobile. Responsive design isn’t optional.
Database Schema Changes: Early schema modifications were painful because I didn’t plan for evolution. Design for change from the beginning.
Business Lessons
User Feedback Drives Everything: Features I thought were essential often went unused, while simple improvements users requested became the most valuable.
Pricing is Continuous Experimentation: Don’t stress about perfect initial pricing. It’s easier to adjust based on user behavior than to predict optimal pricing.
Customer Support is Product Development: Support conversations reveal pain points better than any analytics dashboard.
Personal Development Insights
Learning by Building is Powerful: This project taught me more about web development than any course or tutorial.
Start Before You’re Ready: I began with basic React knowledge. The combination of building something meaningful and learning necessary skills created unstoppable motivation.
Document the Journey: Writing about development clarified my thinking and connected me with other developers facing similar challenges.
What’s Next: Realistic Near-term Plans
At this stage of life, I’m more focused on sustainable progress than grand visions. Here’s what I’m actually working on:
Immediate Priorities (Next 2-3 months)
Mobile Experience Optimization: The current responsive design works on mobile, but the creation flow could be more touch-friendly. Small UI improvements that don’t require a complete rewrite.
User Onboarding Improvement: Many users sign up but never complete their first generation. Better guidance and examples could help.
Performance Optimization: Some database queries are getting slow as usage grows. Time to add strategic indexes and optimize the hot paths.
Medium-term Goals (6 months)
Enhanced Audio Quality: The current AI models are good but not great. Upgrading to newer models when they become cost-effective.
Basic Analytics Dashboard: Users want to see their usage history and favorite generations. Nothing fancy, just useful data presentation.
Simple API Access: A few developers have asked about programmatic access. A basic REST API for generation could open new use cases.
I’m deliberately keeping the scope manageable. At 37 with a business to run, I can only work on this during evenings and weekends, so every feature needs to be carefully prioritized.
Advice for Fellow Career Changers
Engineering Skills Transfer More Than You Think
Problem decomposition, systematic testing, reading technical documentation, and iterative improvement – these engineering fundamentals apply directly to programming. Your analytical mindset is an advantage, even if the syntax is new.
Start with a Real Problem
Don’t build another todo app. Find something that frustrates you in your actual work or life. Having domain expertise in the problem you’re solving gives you a huge advantage over generic tutorials.
Embrace the Design-Build-Test Cycle
In engineering, we design, build prototypes, test under load, and iterate. Software development follows the same cycle – it’s just faster and you can test more scenarios. Your instinct to validate assumptions will serve you well.
Code is Like Engineering Drawings
I initially struggled with “messy” code, but then realized it’s like construction documents. The first draft gets the structure right; then you refine for clarity, efficiency, and maintainability. Perfect is the enemy of shipped.
Use Your Professional Network
Other engineers are curious about tech and often willing to test your projects. My first users were engineering colleagues who needed similar solutions. Don’t underestimate the value of domain-specific feedback.
Conclusion: From Bridges to Bytes
Building musicgeneratorai.io has been the most challenging and rewarding learning experience since my engineering degree. Six months ago, I was designing concrete foundations; today, I’m building digital foundations that serve real users.
The platform isn’t perfect, and I’m not getting rich, but it works reliably – which, as an engineer, is what matters most. More importantly, I proved that the problem-solving skills that make a good engineer also make a capable programmer.
For fellow engineers considering a transition to tech: this honest account shows that your analytical background is a significant advantage. You already understand systems thinking, documentation, testing, and iterative improvement. The syntax is new, but the engineering mindset transfers beautifully.
The most important lesson? Engineering taught me that the best solutions are usually the simplest ones that reliably solve the problem. The same applies to code – elegant simplicity beats complex cleverness every time.
At 37, I’m not abandoning civil engineering to become a full-time developer. I’m adding programming to my professional toolkit, the same way I once learned new CAD software or structural analysis methods. Technology is just another engineering tool – albeit a remarkably powerful one.
Whether you’re designing bridges or building websites, the fundamental engineering principle remains the same: understand the problem, design a solution, build it systematically, test thoroughly, and iterate based on real-world feedback.
Curious about AI music generation? Try musicgeneratorai.io and create your first track in under a minute. Built by an engineer who believes software should be as reliable as a well-designed bridge.
Fellow engineers exploring programming: What projects are you working on? I’d love to hear how your engineering background influences your approach to code. The intersection of traditional engineering and software development continues to fascinate me.
This content originally appeared on DEV Community and was authored by Nico Hayes