Skip to content

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

  1. Single Codebase: Frontend and backend in one project
  2. TypeScript: Full type safety across frontend and backend
  3. Built-in API Routes: No need for separate backend framework
  4. Automatic Optimization: Built-in performance optimizations
  5. Easy Deployment: Deploy to Vercel, Netlify, etc.
  6. Shared Types: Same types for frontend and backend
  7. Middleware Support: Built-in middleware for auth, CORS, etc.
  8. 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

  1. Scalability: Less suitable for very large applications
  2. Complex Business Logic: Can become unwieldy in large projects
  3. Database Connections: Limited connection pooling
  4. Background Jobs: No built-in task queue (need external services)
  5. Microservices: Harder to split into microservices later
  6. 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:

  1. Small Team: You’re likely a small team
  2. Manageable Scope: Construction management is well-defined
  3. Rapid Development: You want to move fast
  4. Type Safety: Shared types between frontend/backend
  5. AI Integration: Easy to integrate AI services
  6. 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

lib/permissions.ts
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.ts
export 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

app/api/projects/route.ts
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

app/api/projects/[id]/units/route.ts
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

lib/validation/unit.ts
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

app/api/escrow/route.ts
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

app/api/escrow/[id]/release/route.ts
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

lib/ai-service.ts
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

app/api/ai/create-units/route.ts
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

lib/websocket.ts
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

app/api/tenders/[id]/bids/route.ts
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

lib/job-queue.ts
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

app/api/jobs/process-file/route.ts
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 AreaNext.js Solution
PermissionsMiddleware + role-based guards
Unit ManagementValidation schemas + dynamic forms
FinancialExternal payment services (Stripe)
AI IntegrationExternal AI service APIs
Real-timeExternal WebSocket service (Pusher)
Background JobsExternal job queue (Bull/BullMQ)

πŸš€ Why This Works for Solo Development

  1. External Services: Handle complex features without building them
  2. TypeScript: Catch errors early and maintain code quality
  3. Modular Architecture: Keep code organized and maintainable
  4. API Routes: Simple, focused endpoints for each feature
  5. Easy Testing: Test each route independently

This approach gives you the power of complex features while keeping the codebase manageable for a solo developer!