Building Scalable Web Applications with Next.js and Node.js
A comprehensive guide to building scalable web applications using Next.js for the frontend and Node.js for the backend.
> Project initialization started...
> Setting up development environment...
_
1. Project Setup and Dependencies
Let's start by creating a new Next.js project with TypeScript and setting up our development environment.
# Create a new Next.js project
npx create-next-app@latest my-scalable-app --typescript --tailwind --eslintAfter the project is created, install additional dependencies for our backend:
# Install backend dependencies
pnpm add express mongoose dotenv cors helmet morgan2. Project Structure
my-scalable-app/ ├── src/ │ ├── app/ │ │ ├── api/ │ │ │ └── routes/ │ │ ├── components/ │ │ ├── lib/ │ │ └── models/ │ ├── pages/ │ └── styles/ ├── public/ ├── server/ │ ├── config/ │ ├── controllers/ │ ├── middleware/ │ ├── models/ │ ├── routes/ │ └── server.ts └── package.json
3. Backend Setup (Node.js & Express)
Create the server configuration file:
// server/config/config.ts
import dotenv from 'dotenv';
dotenv.config();
export const config = {
port: process.env.PORT || 5000,
mongoUri: process.env.MONGODB_URI || 'mongodb://localhost:27017/scalable-app',
nodeEnv: process.env.NODE_ENV || 'development',
corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:3000'
};Set up the Express server:
// server/server.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import { config } from './config/config';
import mongoose from 'mongoose';
const app = express();
// Middleware
app.use(cors({ origin: config.corsOrigin }));
app.use(helmet());
app.use(morgan('dev'));
app.use(express.json());
// Connect to MongoDB
mongoose.connect(config.mongoUri)
.then(() => console.log('Connected to MongoDB'))
.catch((err) => console.error('MongoDB connection error:', err));
// Start server
app.listen(config.port, () => {
console.log(`Server running on port ${config.port}`);
});4. Frontend Setup (Next.js)
Create a custom API client for making requests to our backend:
// src/lib/api-client.ts
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000',
headers: {
'Content-Type': 'application/json'
}
});
// Request interceptor
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized access
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default apiClient;5. Data Models
Create MongoDB models for your data:
// server/models/User.ts
import mongoose from 'mongoose';
import bcrypt from 'bcryptjs';
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 6
}
}, {
timestamps: true
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (this.isModified('password')) {
this.password = await bcrypt.hash(this.password, 10);
}
next();
});
export const User = mongoose.model('User', userSchema);6. API Routes and Controllers
Create API routes and controllers:
// server/controllers/userController.ts
import { Request, Response } from 'express';
import { User } from '../models/User';
import jwt from 'jsonwebtoken';
export const userController = {
async register(req: Request, res: Response) {
try {
const user = new User(req.body);
await user.save();
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET || 'secret',
{ expiresIn: '24h' }
);
res.status(201).json({ user, token });
} catch (error) {
res.status(400).json({ error: error.message });
}
},
async login(req: Request, res: Response) {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
throw new Error('User not found');
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
throw new Error('Invalid credentials');
}
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET || 'secret',
{ expiresIn: '24h' }
);
res.json({ user, token });
} catch (error) {
res.status(400).json({ error: error.message });
}
}
};7. Frontend Components
Create reusable components with proper TypeScript types:
// src/components/Button.tsx
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'accent';
isLoading?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
isLoading = false,
className = '',
...props
}) => {
const baseStyles = 'px-4 py-2 rounded-lg font-pixel transition-all';
const variantStyles = {
primary: 'bg-primary/80 hover:bg-primary text-background',
secondary: 'bg-secondary/80 hover:bg-secondary text-background',
accent: 'bg-accent/80 hover:bg-accent text-background'
};
return (
<button
className={`${baseStyles} ${variantStyles[variant]} ${className}`}
disabled={isLoading}
{...props}
>
{isLoading ? (
<div className="flex items-center justify-center">
<div className="animate-spin h-5 w-5 border-2 border-background/20 border-t-background rounded-full" />
</div>
) : children}
</button>
);
};8. State Management
Implement a custom hook for state management:
// src/hooks/useAuth.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import apiClient from '@/lib/api-client';
interface AuthState {
user: any | null;
token: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}
export const useAuth = create<AuthState>()(
persist(
(set) => ({
user: null,
token: null,
isAuthenticated: false,
login: async (email: string, password: string) => {
try {
const response = await apiClient.post('/auth/login', {
email,
password,
});
set({
user: response.data.user,
token: response.data.token,
isAuthenticated: true,
});
} catch (error) {
throw error;
}
},
logout: () => {
set({
user: null,
token: null,
isAuthenticated: false,
});
},
}),
{
name: 'auth-storage',
}
)
);9. Error Handling
Create a custom error handling middleware:
// server/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
export class AppError extends Error {
statusCode: number;
status: string;
isOperational: boolean;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
export const errorHandler = (
err: AppError,
req: Request,
res: Response,
next: NextFunction
) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
if (process.env.NODE_ENV === 'development') {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
} else {
// Production
if (err.isOperational) {
res.status(err.statusCode).json({
status: err.status,
message: err.message
});
} else {
console.error('ERROR 💥', err);
res.status(500).json({
status: 'error',
message: 'Something went wrong!'
});
}
}
};10. Testing Setup
Set up testing with Jest and React Testing Library:
// jest.config.js
const nextJest = require('next/jest');
const createJestConfig = nextJest({
dir: './',
});
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleDirectories: ['node_modules', '<rootDir>/'],
testEnvironment: 'jest-environment-jsdom',
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/src/components/$1',
'^@/lib/(.*)$': '<rootDir>/src/lib/$1',
},
};
module.exports = createJestConfig(customJestConfig);> Guide completed successfully!
> Happy coding!
_
Next Steps
- • Implement authentication middleware
- • Add rate limiting for API routes
- • Set up CI/CD pipeline
- • Add monitoring and logging
- • Implement caching strategies