Modern Full Stack Development in 2024: The Complete Guide
The full-stack development landscape has evolved dramatically in recent years. With new frameworks, tools, and best practices emerging constantly, it can be challenging to know what to focus on. Here's my comprehensive guide to modern full-stack development in 2024.
The Modern Stack
Frontend: React Ecosystem
Next.js 14 has become the de facto standard for React applications:
// app/page.tsx - App Router with Server Components
export default async function HomePage() {
const posts = await getPosts(); // Server-side data fetching
return (
<div>
<h1>Welcome to My Blog</h1>
<PostList posts={posts} />
</div>
);
}
Key Features:
- App Router with Server Components
- Built-in TypeScript support
- Automatic code splitting
- Image optimization
- API routes
State Management: Zustand
Zustand has emerged as a lightweight alternative to Redux:
import { create } from 'zustand';
const useStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}));
// Usage in component
function Profile() {
const { user, logout } = useStore();
return (
<div>
<h2>Welcome, {user?.name}</h2>
<button onClick={logout}>Logout</button>
</div>
);
}
Styling: Tailwind CSS
Tailwind CSS continues to dominate:
<div className="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl">
<div className="md:flex">
<div className="md:shrink-0">
<img
className="h-48 w-full object-cover md:h-full md:w-48"
src="/img/building.jpg"
alt="Building"
/>
</div>
<div className="p-8">
<div className="uppercase tracking-wide text-sm text-indigo-500 font-semibold">
Company retreat
</div>
<p className="mt-2 text-slate-500">
Looking to take your team away on a retreat to enjoy awesome food and
take in some sunshine? We have a list of places to do just that.
</p>
</div>
</div>
</div>
Backend: Node.js and Beyond
API Development with Express.js
import express from 'express';
import { z } from 'zod';
import rateLimit from 'express-rate-limit';
const app = express();
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
});
app.use(limiter);
app.use(express.json());
// Input validation with Zod
const userSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(2),
});
app.post('/api/users', async (req, res) => {
try {
const userData = userSchema.parse(req.body);
const user = await createUser(userData);
res.json({ user });
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ errors: error.errors });
}
res.status(500).json({ error: 'Internal server error' });
}
});
Database: PostgreSQL with Prisma
Prisma has revolutionized database access in Node.js:
// schema.prisma
model User {
id String @id @default(cuid())
email String @unique
name String
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Using Prisma Client
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Create user with posts
const user = await prisma.user.create({
data: {
email: 'john@example.com',
name: 'John Doe',
posts: {
create: [
{ title: 'My first post', content: 'Hello world!' },
{ title: 'My second post', content: 'This is great!' },
],
},
},
include: {
posts: true,
},
});
Authentication: NextAuth.js
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';
const handler = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!
})
],
callbacks: {
session: async ({ session, token }) => {
if (session?.user) {
session.user.id = token.sub;
}
return session;
},
jwt: async ({ user, token }) => {
if (user) {
token.sub = user.id;
}
return token;
}
}
});
export { handler as GET, handler as POST };
Deployment and DevOps
Docker Configuration
# Multi-stage build for Next.js
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
CI/CD with GitHub Actions
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm run test
- run: npm run build
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
Testing Strategy
Unit Testing with Vitest
// utils.test.ts
import { describe, it, expect } from 'vitest';
import { formatCurrency, validateEmail } from './utils';
describe('Utils', () => {
it('should format currency correctly', () => {
expect(formatCurrency(1234.56)).toBe('$1,234.56');
expect(formatCurrency(0)).toBe('$0.00');
});
it('should validate email addresses', () => {
expect(validateEmail('test@example.com')).toBe(true);
expect(validateEmail('invalid-email')).toBe(false);
});
});
Integration Testing with Playwright
// tests/auth.spec.ts
import { test, expect } from '@playwright/test';
test('user can sign in and access dashboard', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Welcome back');
});
Performance Optimization
Code Splitting and Lazy Loading
// Dynamic imports for code splitting
import dynamic from 'next/dynamic';
const DynamicChart = dynamic(() => import('../components/Chart'), {
loading: () => <p>Loading chart...</p>,
ssr: false,
});
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<DynamicChart />
</div>
);
}
Image Optimization
import Image from 'next/image';
function Hero() {
return (
<div className="relative h-96">
<Image
src="/hero-image.jpg"
alt="Hero image"
fill
className="object-cover"
priority
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
);
}
Monitoring and Analytics
Error Tracking with Sentry
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
debug: false,
replaysOnErrorSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
integrations: [
new Sentry.Replay({
maskAllText: true,
blockAllMedia: true,
}),
],
});
Analytics with Vercel Analytics
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
);
}
Best Practices for 2024
1. Type Safety First
Use TypeScript everywhere and leverage tools like Zod for runtime validation.
2. Server Components by Default
Use React Server Components for better performance and SEO.
3. Progressive Enhancement
Build applications that work without JavaScript and enhance with it.
4. Security by Design
Implement security measures from the beginning, not as an afterthought.
5. Performance Budgets
Set and monitor performance budgets for your applications.
Conclusion
Modern full-stack development in 2024 is about choosing the right tools for the job and following established patterns. The ecosystem has matured significantly, with excellent tooling for:
- Frontend: Next.js with React Server Components
- Backend: Node.js with Express or tRPC
- Database: PostgreSQL with Prisma
- Authentication: NextAuth.js
- Deployment: Vercel or Docker containers
- Testing: Vitest and Playwright
The key is to start simple, focus on user experience, and scale thoughtfully as your application grows.
Want to dive deeper into any of these topics? Join our developer community or reach out for personalized guidance!




