logo
Back to Blog
Modern Full Stack Development in 2024: The Complete Guide
Full StackReactNext.jsNode.jsTypeScript

Modern Full Stack Development in 2024: The Complete Guide

A comprehensive guide to the modern full-stack development landscape, including the best tools, frameworks, and practices for 2024.

Profile image 1Profile image 2Profile image 3Profile image 4
Nikola Lalovic
January 10, 2024
7 min read

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!

Related Articles

SaaSArchitecture

Building Scalable SaaS Architecture: Lessons from the Trenches

Learn the key architectural decisions that can make or break your SaaS application as it scales from 100 to 100,000+ users.

Read More

Found This Helpful?

If you enjoyed this article and want to discuss your project or get personalized advice, I'd love to hear from you.