Full-Stack Next.js Example
Yes, absolutely! Next.js can handle backend functionality through its API Routes feature. Hereβs how it would work for your 14Build project:
π’ Next.js Backend Capabilities
API Routes Structure
clerk-front/βββ app/β βββ api/ # Backend API routesβ β βββ users/β β β βββ route.ts # GET, POST /api/usersβ β β βββ [id]/β β β βββ route.ts # GET, PUT, DELETE /api/users/[id]β β βββ projects/β β β βββ route.ts # GET, POST /api/projectsβ β β βββ [id]/β β β βββ route.ts # GET, PUT, DELETE /api/projects/[id]β β β βββ units/β β β βββ route.ts # GET, POST /api/projects/[id]/unitsβ β βββ soq/β β β βββ route.ts # GET, POST /api/soqβ β β βββ [id]/β β β βββ route.ts # GET, PUT, DELETE /api/soq/[id]β β βββ tenders/β β β βββ route.ts # GET, POST /api/tendersβ β β βββ [id]/β β β βββ route.ts # GET, PUT, DELETE /api/tenders/[id]β β β βββ bids/β β β βββ route.ts # GET, POST /api/tenders/[id]/bidsβ β βββ escrow/β β β βββ route.ts # GET, POST /api/escrowβ β β βββ [id]/β β β βββ route.ts # GET, PUT, DELETE /api/escrow/[id]β β βββ auth/β β β βββ login/β β β β βββ route.ts # POST /api/auth/loginβ β β βββ register/β β β βββ route.ts # POST /api/auth/registerβ β βββ ai/β β βββ create-units/β β β βββ route.ts # POST /api/ai/create-unitsβ β βββ create-jobs/β β βββ route.ts # POST /api/ai/create-jobsβ βββ (frontend routes) # Your existing frontend pagesβ βββ globals.cssβββ lib/β βββ db.ts # Database connectionβ βββ auth.ts # Authentication utilitiesβ βββ validation.ts # Input validationβ βββ utils.ts # Utility functionsβββ types/β βββ api.ts # API typesβ βββ user.ts # User typesβ βββ project.ts # Project typesβ βββ ...βββ package.jsonπ Example API Routes
app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';import { prisma } from '@/lib/db';import { validateUser } from '@/lib/validation';
export async function GET(request: NextRequest) { try { const users = await prisma.user.findMany({ include: { entity: true, category: true, }, });
return NextResponse.json(users); } catch (error) { return NextResponse.json( { error: 'Failed to fetch users' }, { status: 500 } ); }}
export async function POST(request: NextRequest) { try { const body = await request.json(); const validatedData = validateUser(body);
const user = await prisma.user.create({ data: validatedData, include: { entity: true, category: true, }, });
return NextResponse.json(user, { status: 201 }); } catch (error) { return NextResponse.json( { error: 'Failed to create user' }, { status: 400 } ); }}app/api/projects/route.ts
import { NextRequest, NextResponse } from 'next/server';import { prisma } from '@/lib/db';import { getCurrentUser } from '@/lib/auth';
export async function GET(request: NextRequest) { try { const currentUser = await getCurrentUser(request); const { searchParams } = new URL(request.url); const status = searchParams.get('status');
const projects = await prisma.project.findMany({ where: { ...(status && { status }), OR: [ { ownerId: currentUser.id }, { teamMembers: { some: { userId: currentUser.id } } }, ], }, include: { owner: true, units: true, teamMembers: { include: { user: true }, }, }, });
return NextResponse.json(projects); } catch (error) { return NextResponse.json( { error: 'Failed to fetch projects' }, { status: 500 } ); }}
export async function POST(request: NextRequest) { try { const currentUser = await getCurrentUser(request); const body = await request.json();
const project = await prisma.project.create({ data: { ...body, ownerId: currentUser.id, teamMembers: { create: { userId: currentUser.id, role: 'Project Owner', }, }, }, include: { owner: true, units: true, }, });
return NextResponse.json(project, { status: 201 }); } catch (error) { return NextResponse.json( { error: 'Failed to create project' }, { status: 400 } ); }}app/api/ai/create-units/route.ts
import { NextRequest, NextResponse } from 'next/server';import { getCurrentUser } from '@/lib/auth';
export async function POST(request: NextRequest) { try { const currentUser = await getCurrentUser(request); const { projectId, numberOfUnits, unitType } = await request.json();
// Call your AI service here const aiResponse = await fetch('your-ai-endpoint', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ numberOfUnits, unitType, projectId, }), });
const units = await aiResponse.json();
// Save units to database const savedUnits = await prisma.unit.createMany({ data: units.map((unit: any) => ({ ...unit, projectId, })), });
return NextResponse.json(savedUnits); } catch (error) { return NextResponse.json( { error: 'Failed to create units with AI' }, { status: 500 } ); }}π’ Pros of Next.js Backend
β Advantages
- Single Codebase: Frontend and backend in one project
- TypeScript: Full type safety across frontend and backend
- Built-in API Routes: No need for separate backend framework
- Automatic Optimization: Built-in performance optimizations
- Easy Deployment: Deploy to Vercel, Netlify, etc.
- Shared Types: Same types for frontend and backend
- Middleware Support: Built-in middleware for auth, CORS, etc.
- File-based Routing: Intuitive API route structure
β Perfect for Your Use Case
- Small to Medium Projects: Ideal for 14Buildβs scope
- Rapid Development: Faster iteration with single codebase
- Type Safety: Shared TypeScript types
- AI Integration: Easy to integrate AI services
- Authentication: Works well with Clerk
π΄ Cons of Next.js Backend
β Limitations
- Scalability: Less suitable for very large applications
- Complex Business Logic: Can become unwieldy in large projects
- Database Connections: Limited connection pooling
- Background Jobs: No built-in task queue (need external services)
- Microservices: Harder to split into microservices later
- Team Size: Can be challenging with large development teams
π€ When to Choose Next.js Backend
β Choose Next.js Backend When:
- Team size < 10 developers
- Project scope is manageable
- You want rapid development
- You prefer single codebase
- Youβre deploying to Vercel/Netlify
- You need shared types between frontend/backend
β Choose Separate Backend When:
- Team size > 10 developers
- Complex business logic
- Need for microservices
- Heavy background processing
- Multiple frontend applications
- Need for advanced caching strategies
π Recommendation for 14Build
For your current project, Next.js backend would work well because:
- Small Team: Youβre likely a small team
- Manageable Scope: Construction management is well-defined
- Rapid Development: You want to move fast
- Type Safety: Shared types between frontend/backend
- AI Integration: Easy to integrate AI services
- Deployment: Simple deployment to Vercel
Great question! Let me show you how to solve those complex areas using Next.js API Routes with external services and smart architecture:
π’ Solving Complex Areas with Next.js
1. Permission System π
Middleware-based Permission System
export enum Permission { CREATE_PROJECT = 'create_project', CREATE_TENDER = 'create_tender', CREATE_SOQ = 'create_soq', AWARD_BID = 'award_bid', DELETE_PROJECT = 'delete_project', MANAGE_USERS = 'manage_users',}
export const rolePermissions = { 'Project Owner': [ Permission.CREATE_PROJECT, Permission.CREATE_TENDER, Permission.CREATE_SOQ, Permission.AWARD_BID, // NO DELETE_PROJECT ], 'In-House User': [ Permission.CREATE_TENDER, Permission.CREATE_SOQ, Permission.AWARD_BID, // NO DELETE_PROJECT, NO MANAGE_USERS ], 'Bidder': [ // Can only bid on tenders ],};
// lib/auth-guard.tsexport async function requirePermission( request: NextRequest, permission: Permission) { const user = await getCurrentUser(request); const userRole = await getUserRole(user.id); const hasPermission = rolePermissions[userRole]?.includes(permission);
if (!hasPermission) { throw new Error('Insufficient permissions'); }}API Route with Permissions
import { requirePermission, Permission } from '@/lib/auth-guard';
export async function POST(request: NextRequest) { try { // Check permission before processing await requirePermission(request, Permission.CREATE_PROJECT);
const body = await request.json(); const project = await prisma.project.create({ data: body, });
return NextResponse.json(project); } catch (error) { return NextResponse.json( { error: error.message }, { status: 403 } ); }}2. Unit Management ποΈ
Dynamic Unit Configuration
export async function POST( request: NextRequest, { params }: { params: { id: string } }) { try { const { units } = await request.json();
// Validate floor areas sum to total for (const unit of units) { const floorSum = unit.floors.reduce((sum: number, floor: any) => sum + floor.floorArea, 0 );
if (Math.abs(floorSum - unit.totalFloorArea) > 0.01) { throw new Error(`Floor areas don't sum to total for unit ${unit.unitNumber}`); } }
const createdUnits = await prisma.unit.createMany({ data: units.map((unit: any) => ({ ...unit, projectId: params.id, floors: JSON.stringify(unit.floors), // Store as JSON })), });
return NextResponse.json(createdUnits); } catch (error) { return NextResponse.json( { error: error.message }, { status: 400 } ); }}Unit Validation Schema
import { z } from 'zod';
export const unitSchema = z.object({ unitNumber: z.string(), totalFloorArea: z.number().positive(), numberOfFloors: z.number().int().min(1), numberOfRooms: z.number().int().min(1), numberOfBathrooms: z.number().int().min(1), floors: z.array(z.object({ floorNumber: z.number().int().min(1), floorArea: z.number().positive(), })).min(1),});
export const validateUnit = (data: any) => { return unitSchema.parse(data);};3. Financial Workflows π°
Escrow Management with External Service
import { stripe } from '@/lib/stripe'; // External payment service
export async function POST(request: NextRequest) { try { const { projectId, amount, milestoneId } = await request.json();
// Create escrow account const escrowAccount = await stripe.accounts.create({ type: 'express', capabilities: { transfers: { requested: true }, }, });
// Create escrow record const escrow = await prisma.escrow.create({ data: { projectId, milestoneId, amount, status: 'PENDING', externalAccountId: escrowAccount.id, }, });
// Create payment intent const paymentIntent = await stripe.paymentIntents.create({ amount: amount * 100, // Convert to cents currency: 'usd', application_fee_amount: Math.round(amount * 0.025 * 100), // 2.5% fee transfer_data: { destination: escrowAccount.id, }, });
return NextResponse.json({ escrow, paymentIntent: paymentIntent.client_secret, }); } catch (error) { return NextResponse.json( { error: 'Failed to create escrow' }, { status: 500 } ); }}Milestone Payment Processing
export async function POST( request: NextRequest, { params }: { params: { id: string } }) { try { const escrow = await prisma.escrow.findUnique({ where: { id: params.id }, include: { milestone: true }, });
if (!escrow) { throw new Error('Escrow not found'); }
// Release funds to contractor const transfer = await stripe.transfers.create({ amount: escrow.amount * 100, currency: 'usd', destination: escrow.externalAccountId, });
// Update escrow status await prisma.escrow.update({ where: { id: params.id }, data: { status: 'RELEASED', releasedAt: new Date(), }, });
// Update milestone status await prisma.milestone.update({ where: { id: escrow.milestoneId }, data: { status: 'PAID' }, });
return NextResponse.json({ success: true }); } catch (error) { return NextResponse.json( { error: 'Failed to release escrow' }, { status: 500 } ); }}4. AI Integration π€
AI Service Integration
export class AIService { private apiKey: string; private baseUrl: string;
constructor() { this.apiKey = process.env.AI_SERVICE_API_KEY!; this.baseUrl = process.env.AI_SERVICE_URL!; }
async createUnits(projectData: any) { const response = await fetch(`${this.baseUrl}/create-units`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(projectData), });
if (!response.ok) { throw new Error('AI service failed'); }
return response.json(); }
async createJobs(soqData: any) { const response = await fetch(`${this.baseUrl}/create-jobs`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(soqData), });
if (!response.ok) { throw new Error('AI service failed'); }
return response.json(); }}
export const aiService = new AIService();AI API Routes
import { aiService } from '@/lib/ai-service';
export async function POST(request: NextRequest) { try { const projectData = await request.json();
// Call AI service const aiUnits = await aiService.createUnits(projectData);
// Validate AI response const validatedUnits = aiUnits.map((unit: any) => validateUnit(unit) );
// Save to database const savedUnits = await prisma.unit.createMany({ data: validatedUnits.map((unit: any) => ({ ...unit, projectId: projectData.projectId, createdByAI: true, })), });
return NextResponse.json(savedUnits); } catch (error) { return NextResponse.json( { error: 'Failed to create units with AI' }, { status: 500 } ); }}5. Real-time Features β‘
WebSocket Integration with External Service
import { pusher } from '@/lib/pusher'; // External WebSocket service
export class WebSocketService { async notifyProjectUpdate(projectId: string, update: any) { await pusher.trigger(`project-${projectId}`, 'project-updated', update); }
async notifyBidSubmitted(tenderId: string, bid: any) { await pusher.trigger(`tender-${tenderId}`, 'bid-submitted', bid); }
async notifyMilestoneCompleted(projectId: string, milestone: any) { await pusher.trigger(`project-${projectId}`, 'milestone-completed', milestone); }}
export const wsService = new WebSocketService();Real-time Updates in API Routes
import { wsService } from '@/lib/websocket';
export async function POST( request: NextRequest, { params }: { params: { id: string } }) { try { const bidData = await request.json();
const bid = await prisma.bid.create({ data: { ...bidData, tenderId: params.id, }, });
// Notify real-time subscribers await wsService.notifyBidSubmitted(params.id, bid);
return NextResponse.json(bid); } catch (error) { return NextResponse.json( { error: 'Failed to submit bid' }, { status: 500 } ); }}6. Background Jobs π
External Job Queue Service
import { queue } from '@/lib/bull'; // External job queue
export class JobQueueService { async processLargeFile(fileId: string) { await queue.add('process-file', { fileId }, { attempts: 3, backoff: { type: 'exponential', delay: 2000, }, }); }
async generateReport(projectId: string) { await queue.add('generate-report', { projectId }, { priority: 1, }); }
async syncWithBlockchain(data: any) { await queue.add('blockchain-sync', data, { delay: 5000, // 5 second delay }); }}
export const jobQueue = new JobQueueService();Job Processing Route
import { jobQueue } from '@/lib/job-queue';
export async function POST(request: NextRequest) { try { const { fileId } = await request.json();
// Queue the job instead of processing immediately await jobQueue.processLargeFile(fileId);
return NextResponse.json({ message: 'File processing queued', jobId: fileId }); } catch (error) { return NextResponse.json( { error: 'Failed to queue job' }, { status: 500 } ); }}π― Key Solutions Summary
| Complex Area | Next.js Solution |
|---|---|
| Permissions | Middleware + role-based guards |
| Unit Management | Validation schemas + dynamic forms |
| Financial | External payment services (Stripe) |
| AI Integration | External AI service APIs |
| Real-time | External WebSocket service (Pusher) |
| Background Jobs | External job queue (Bull/BullMQ) |
π Why This Works for Solo Development
- External Services: Handle complex features without building them
- TypeScript: Catch errors early and maintain code quality
- Modular Architecture: Keep code organized and maintainable
- API Routes: Simple, focused endpoints for each feature
- Easy Testing: Test each route independently
This approach gives you the power of complex features while keeping the codebase manageable for a solo developer!