Next.js
Building a Full Next.js Application - Part 2
Building a Full Next.js Application - Part 2
Welcome to Part 2 of building TaskFlow! In this lesson, we'll implement advanced features, add real-time collaboration, optimize performance, write tests, and deploy our application to production.
Step 6: Task Management with Drag-and-Drop
Install drag-and-drop library:
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
components/tasks/TaskBoard.tsx:
'use client';
import { useMemo, useState } from 'react';
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { SortableContext, arrayMove } from '@dnd-kit/sortable';
import { TaskColumn } from './TaskColumn';
import { TaskCard } from './TaskCard';
const COLUMNS = [
{ id: 'TODO', title: 'To Do' },
{ id: 'IN_PROGRESS', title: 'In Progress' },
{ id: 'IN_REVIEW', title: 'In Review' },
{ id: 'DONE', title: 'Done' },
];
export function TaskBoard({ tasks, projectId }: Props) {
const [activeTask, setActiveTask] = useState(null);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
})
);
const tasksByStatus = useMemo(() => {
return COLUMNS.reduce((acc, column) => {
acc[column.id] = tasks.filter((task) => task.status === column.id);
return acc;
}, {} as Record<string, Task[]>);
}, [tasks]);
const handleDragStart = (event: DragStartEvent) => {
const task = tasks.find((t) => t.id === event.active.id);
setActiveTask(task);
};
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) {
setActiveTask(null);
return;
}
const activeTask = tasks.find((t) => t.id === active.id);
const overColumn = over.id as TaskStatus;
if (activeTask && activeTask.status !== overColumn) {
// Update task status
await fetch(\`/api/tasks/${activeTask.id}\`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: overColumn }),
});
// Trigger revalidation or update local state
}
setActiveTask(null);
};
return (
<DndContext
sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="grid grid-cols-4 gap-4 h-full">
{COLUMNS.map((column) => (
<TaskColumn
key={column.id}
column={column}
tasks={tasksByStatus[column.id] || []}
/>
))}
</div>
<DragOverlay>
{activeTask ? <TaskCard task={activeTask} isDragging /> : null}
</DragOverlay>
</DndContext>
);
}
components/tasks/TaskColumn.tsx:
'use client';
import { useDroppable } from '@dnd-kit/core';
import { SortableContext } from '@dnd-kit/sortable';
import { TaskCard } from './TaskCard';
export function TaskColumn({ column, tasks }: Props) {
const { setNodeRef, isOver } = useDroppable({
id: column.id,
});
return (
<div
ref={setNodeRef}
className={cn(
'flex flex-col bg-gray-50 rounded-lg p-4',
isOver && 'bg-blue-50'
)}
>
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold">{column.title}</h3>
<span className="text-sm text-gray-500">{tasks.length}</span>
</div>
<SortableContext items={tasks.map((t) => t.id)}>
<div className="space-y-2 flex-1 overflow-y-auto">
{tasks.map((task) => (
<TaskCard key={task.id} task={task} />
))}
</div>
</SortableContext>
</div>
);
}
Step 7: Real-time Collaboration with Pusher
lib/pusher.ts:
import Pusher from 'pusher';
import PusherClient from 'pusher-js';
// Server-side Pusher instance
export const pusherServer = new Pusher({
appId: process.env.PUSHER_APP_ID!,
key: process.env.NEXT_PUBLIC_PUSHER_KEY!,
secret: process.env.PUSHER_SECRET!,
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
useTLS: true,
});
// Client-side Pusher instance
export const pusherClient = new PusherClient(
process.env.NEXT_PUBLIC_PUSHER_KEY!,
{
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
}
);
app/api/tasks/[id]/route.ts (with real-time updates):
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { prisma } from '@/lib/db';
import { pusherServer } from '@/lib/pusher';
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { status, priority, assigneeId, title, description, dueDate } = body;
const task = await prisma.task.update({
where: { id: params.id },
data: {
...(status && { status }),
...(priority && { priority }),
...(assigneeId !== undefined && { assigneeId }),
...(title && { title }),
...(description !== undefined && { description }),
...(dueDate !== undefined && { dueDate }),
},
include: {
assignee: {
select: {
id: true,
name: true,
image: true,
},
},
},
});
// Trigger real-time update
await pusherServer.trigger(
\`project-${task.projectId}\`,
'task-updated',
{
task,
updatedBy: {
id: session.user.id,
name: session.user.name,
},
}
);
return NextResponse.json(task);
}
hooks/useRealtimeUpdates.ts:
import { useEffect } from 'react';
import { pusherClient } from '@/lib/pusher';
import { useRouter } from 'next/navigation';
export function useRealtimeUpdates(projectId: string) {
const router = useRouter();
useEffect(() => {
const channel = pusherClient.subscribe(\`project-${projectId}\`);
channel.bind('task-updated', (data: any) => {
// Revalidate the current page
router.refresh();
// Show notification
showNotification({
title: 'Task Updated',
message: \`${data.updatedBy.name} updated a task\`,
});
});
channel.bind('task-created', (data: any) => {
router.refresh();
showNotification({
title: 'New Task',
message: \`${data.createdBy.name} created a task\`,
});
});
channel.bind('comment-added', (data: any) => {
router.refresh();
showNotification({
title: 'New Comment',
message: \`${data.author.name} commented on a task\`,
});
});
return () => {
channel.unbind_all();
pusherClient.unsubscribe(\`project-${projectId}\`);
};
}, [projectId, router]);
}
Step 8: Testing Implementation
Install testing dependencies:
npm install -D @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom npm install -D @playwright/test
jest.config.js:
const nextJest = require('next/jest');
const createJestConfig = nextJest({
dir: './',
});
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jest-environment-jsdom',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
},
};
module.exports = createJestConfig(customJestConfig);
__tests__/components/TaskCard.test.tsx:
import { render, screen } from '@testing-library/react';
import { TaskCard } from '@/components/tasks/TaskCard';
describe('TaskCard', () => {
const mockTask = {
id: '1',
title: 'Test Task',
description: 'Test description',
status: 'TODO',
priority: 'HIGH',
dueDate: new Date('2024-12-31'),
assignee: {
id: 'user1',
name: 'John Doe',
image: '/avatar.jpg',
},
};
it('renders task title', () => {
render(<TaskCard task={mockTask} />);
expect(screen.getByText('Test Task')).toBeInTheDocument();
});
it('displays priority badge', () => {
render(<TaskCard task={mockTask} />);
expect(screen.getByText('HIGH')).toBeInTheDocument();
});
it('shows assignee information', () => {
render(<TaskCard task={mockTask} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
e2e/project-creation.spec.ts (Playwright):
import { test, expect } from '@playwright/test';
test('user can create a new project', async ({ page }) => {
// Login
await page.goto('http://localhost:3000/login');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Navigate to projects
await page.waitForURL('**/dashboard');
await page.click('text=New Project');
// Fill project form
await page.fill('input[name="name"]', 'Test Project');
await page.fill('textarea[name="description"]', 'This is a test project');
await page.click('button:has-text("Create")');
// Verify project created
await expect(page.locator('text=Test Project')).toBeVisible();
});
Step 9: Performance Optimization
Implement pagination for tasks:
// app/api/projects/[id]/tasks/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '20');
const skip = (page - 1) * limit;
const [tasks, total] = await Promise.all([
prisma.task.findMany({
where: { projectId: params.id },
skip,
take: limit,
include: {
assignee: true,
},
orderBy: {
createdAt: 'desc',
},
}),
prisma.task.count({
where: { projectId: params.id },
}),
]);
return NextResponse.json({
tasks,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
}
Add database indexes (prisma/schema.prisma):
model Task {
// ... fields
@@index([projectId])
@@index([assigneeId])
@@index([status])
@@index([createdAt])
}
model Project {
// ... fields
@@index([slug])
@@index([archived])
}
Implement caching for frequently accessed data:
import { unstable_cache } from 'next/cache';
export const getProject = unstable_cache(
async (id: string) => {
return await prisma.project.findUnique({
where: { id },
include: {
members: {
include: {
user: true,
},
},
_count: {
select: {
tasks: true,
},
},
},
});
},
['project'],
{
revalidate: 60, // Cache for 60 seconds
tags: [\`project-${id}\`],
}
);
Step 10: Production Deployment
Environment variables for production (.env.production):
# Database DATABASE_URL="postgresql://user:password@production-host:5432/taskflow" # Auth NEXTAUTH_URL="https://taskflow.example.com" NEXTAUTH_SECRET="secure-production-secret" # OAuth GOOGLE_CLIENT_ID="production-google-client-id" GOOGLE_CLIENT_SECRET="production-google-client-secret" # Pusher NEXT_PUBLIC_PUSHER_KEY="production-pusher-key" PUSHER_SECRET="production-pusher-secret" PUSHER_APP_ID="production-app-id" NEXT_PUBLIC_PUSHER_CLUSTER="mt1" # Monitoring SENTRY_DSN="your-sentry-dsn" NEXT_PUBLIC_VERCEL_ANALYTICS_ID="your-analytics-id"
Deploy to Vercel:
# Install Vercel CLI npm install -g vercel # Login vercel login # Deploy vercel --prod # Or use Git integration for automatic deployments git push origin main
Production Checklist
Pre-Deployment Checklist:
- ✓ All environment variables configured in Vercel
- ✓ Database migrations run on production database
- ✓ SSL certificate configured (automatic with Vercel)
- ✓ Custom domain connected
- ✓ Error tracking configured (Sentry)
- ✓ Analytics configured (Vercel Analytics, GA4)
- ✓ Security headers configured
- ✓ Rate limiting implemented
- ✓ Database backups scheduled
- ✓ Monitoring and alerts set up
Security Hardening
middleware.ts (rate limiting & security):
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
});
export async function middleware(request: NextRequest) {
// Rate limiting for API routes
if (request.nextUrl.pathname.startsWith('/api/')) {
const ip = request.ip ?? '127.0.0.1';
const { success } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
}
// Security headers
const response = NextResponse.next();
response.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()'
);
return response;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};
Monitoring & Observability
lib/monitoring.ts:
import * as Sentry from '@sentry/nextjs';
export function logError(error: Error, context?: Record<string, any>) {
console.error(error);
if (process.env.NODE_ENV === 'production') {
Sentry.captureException(error, {
extra: context,
});
}
}
export function logPerformance(metric: string, duration: number) {
if (process.env.NODE_ENV === 'production') {
// Send to your analytics service
fetch('/api/analytics/performance', {
method: 'POST',
body: JSON.stringify({ metric, duration }),
keepalive: true,
});
}
}
Documentation & Maintenance
README.md structure:
# TaskFlow Project management application built with Next.js 15 ## Features - Authentication with NextAuth.js - Real-time collaboration with Pusher - Drag-and-drop task boards - Team management - Notifications ## Tech Stack - Next.js 15 - TypeScript - PostgreSQL + Prisma - Tailwind CSS - Pusher ## Getting Started \`\`\`bash npm install cp .env.example .env.local # Configure environment variables npx prisma db push npm run dev \`\`\` ## Testing \`\`\`bash npm test # Unit tests npm run test:e2e # E2E tests \`\`\` ## Deployment See [DEPLOYMENT.md](./DEPLOYMENT.md) ## License MIT
Final Project Exercise:
- Complete the TaskFlow application with all features
- Write unit tests for key components
- Write E2E tests for critical user flows
- Optimize database queries and add proper indexes
- Set up error tracking with Sentry
- Configure CI/CD with GitHub Actions
- Deploy to Vercel with custom domain
- Set up monitoring and alerts
- Document your API with Swagger/OpenAPI
- Create a comprehensive README
Congratulations!
You've now built a complete, production-ready Next.js application from scratch. You've learned:
- Project architecture and planning
- Database design with Prisma
- Authentication with NextAuth.js
- Real-time features with Pusher
- Advanced UI with drag-and-drop
- Testing strategies (unit and E2E)
- Performance optimization
- Security best practices
- Production deployment
- Monitoring and observability
These skills form the foundation for building any modern web application with Next.js!
Next Steps:
- Add more features: notifications, search, analytics dashboard
- Implement PWA capabilities for offline support
- Add internationalization (i18n) for multiple languages
- Build a mobile app with React Native sharing code
- Explore edge runtime for better performance
- Implement advanced caching strategies
- Add AI features with OpenAI or similar APIs
- Build integrations with third-party services