Overview
Backend-development
Backend Development: Building APIs
Estimated reading time: ~40 minutes
Table of Contents
- Introduction to Backend Development
- Understanding APIs
- Project Setup
- Database Setup
- API Implementation
- Authentication & Security
- Best Practices
- Next Steps
- Additional Resources
Introduction to Backend Development
Backend development involves creating server-side applications that power websites and apps. It focuses on databases, APIs, authentication, and business logic. The backend manages data processing, storage, and security while enabling the frontend to interact with data seamlessly.
Explanation:
- The backend is responsible for handling data operations, storing information in databases, and implementing business logic that the frontend relies on.
- A server-side application typically listens to incoming requests from clients (e.g., browsers, mobile apps), processes these requests, interacts with databases, and sends back a response.
- Backend concerns also include security measures (like authentication and authorization), performance optimizations, and maintaining data integrity.
Understanding APIs
What is an API?
An API (Application Programming Interface) defines how different software components should interact. In web development, APIs allow frontends to communicate with backends through standardized protocols and data formats.
Explanation:
- An API acts like a contract that specifies how to request or send data to the server and what format this data should be in.
- Common communication protocols include HTTP/HTTPS.
- Data is often transferred in JSON format for simplicity and readability.
API Architecture Styles
Several API architectures exist for different use cases:
- REST (Representational State Transfer): Uses HTTP methods and is stateless
- GraphQL: Enables clients to request specific data shapes
- gRPC: Uses Protocol Buffers for efficient communication
- WebSocket: Enables real-time bidirectional communication
We’ll build a RESTful API for its simplicity and widespread adoption.
Explanation:
- REST is one of the most popular architectures, using HTTP methods (GET, POST, PUT, DELETE, etc.). It is stateless, meaning the server does not store any session information about the client between requests.
- Other styles have specific advantages and use cases, but REST is straightforward to learn and widely supported.
Project Setup
Prerequisites
Install these tools:
- Node.js 18+ - Download
- PostgreSQL 14+ - Download
- TypeScript 5+ - We’ll install this with our project
- A REST client (Postman/Insomnia) for testing
Explanation:
- Node.js: A JavaScript runtime for server-side execution.
- PostgreSQL: A relational database management system we’ll use for storing data.
- TypeScript: A superset of JavaScript that adds static typing, helping catch errors early in development.
- REST client: Tools like Postman or Insomnia are used to send HTTP requests to our API for testing.
Project Structure
First, create your project:
mkdir social-api
cd social-api
npm init -y
Install dependencies:
npm install express pg bcrypt jsonwebtoken cors helmet winston zod express-rate-limit dotenv
npm install --save-dev typescript @types/express @types/pg @types/bcrypt @types/jsonwebtoken @types/cors ts-node-dev @types/node
Create TypeScript configuration:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Update package.json
scripts:
{
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/app.ts",
"build": "tsc",
"start": "node dist/app.js"
}
}
Create this folder structure:
src/
├── config/
│ ├── database.ts
│ └── environment.ts
├── controllers/
│ ├── auth.controller.ts
│ └── post.controller.ts
├── middleware/
│ ├── auth.middleware.ts
│ ├── error.middleware.ts
│ └── validation.middleware.ts
├── routes/
│ ├── auth.routes.ts
│ └── post.routes.ts
├── services/
│ ├── auth.service.ts
│ └── post.service.ts
├── types/
│ ├── auth.types.ts
│ └── post.types.ts
├── utils/
│ ├── logger.ts
│ └── password.ts
└── app.ts
Explanation:
- config: Contains application-wide configuration (e.g., environment variables, database connection).
- controllers: Define request-handling logic. They orchestrate data flow between services and HTTP requests/responses.
- middleware: Functions that process requests/responses at particular stages in the Express pipeline (e.g., error handling, authentication).
- routes: Express routers that map URL paths to controller actions.
- services: Contain business logic or data handling logic (e.g., database queries).
- types: Holds TypeScript type definitions for data transfer objects (DTOs) and other interfaces.
- utils: Utility functions shared across the app (e.g., logger, password helpers).
- app.ts: Main entry point of the application that initializes Express, middleware, and routes.
Database Setup
Configuration
Create .env
in your project root:
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/social_db
JWT_SECRET=your-super-secret-key-min-32-chars
JWT_EXPIRES_IN=1h
CORS_ORIGIN=http://localhost:3000
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
Explanation:
- .env file: Stores environment-specific variables (e.g., database connection string, JWT secret).
- NODE_ENV: Tells our application whether we are in development, production, or test mode.
- JWT_SECRET: Critical for signing JWT tokens; must be kept secure.
- PORT: The port the server will listen on.
Set up environment validation (src/config/environment.ts
):
import { z } from 'zod';
import dotenv from 'dotenv';
dotenv.config();
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
JWT_EXPIRES_IN: z.string(),
CORS_ORIGIN: z.string(),
RATE_LIMIT_WINDOW_MS: z.string().transform(Number),
RATE_LIMIT_MAX_REQUESTS: z.string().transform(Number),
});
const env = envSchema.parse(process.env);
export default env;
Explanation:
- dotenv: Loads environment variables from our
.env
file.- zod: A schema validation library ensuring that environment variables meet certain criteria (e.g., minimum length, valid URLs).
- This code parses the environment variables and validates them, throwing an error if any required variable is missing or invalid.
Set up database connection (src/config/database.ts
):
import { Pool } from 'pg';
import env from './environment';
import logger from '../utils/logger';
const pool = new Pool({
connectionString: env.DATABASE_URL,
ssl: env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
});
pool.on('error', (err) => {
logger.error('Unexpected database error', err);
process.exit(-1);
});
export default pool;
Explanation:
- pg.Pool: Manages connections to the PostgreSQL database.
- In production, SSL is enabled to secure data in transit.
- On database error, we log and exit the process to avoid running in a corrupted state.
Database Schema
Connect to PostgreSQL and create tables:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE followers (
follower_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
following_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (follower_id, following_id)
);
CREATE TABLE likes (
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, post_id)
);
CREATE INDEX users_email_idx ON users(email);
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX likes_post_id_idx ON likes(post_id);
Explanation:
- users table: Stores user accounts with unique emails and usernames.
- posts table: Each post references the user who created it (
user_id
).- followers table: Implements a many-to-many relationship between users (who follows whom).
- likes table: Tracks which user liked which post, also a many-to-many relationship.
- ON DELETE CASCADE: Ensures that when a user is deleted, all related posts/follows/likes from that user are also removed to maintain referential integrity.
- INDEXES: Improve database query performance on frequently searched columns (e.g.,
API Implementation
Types
Create type definitions (src/types/auth.types.ts
):
export interface User {
id: number;
username: string;
email: string;
password: string;
createdAt: Date;
}
export interface RegisterDTO {
username: string;
email: string;
password: string;
}
export interface LoginDTO {
email: string;
password: string;
}
export interface AuthResponse {
token: string;
user: {
id: number;
username: string;
email: string;
};
}
Explanation:
- User interface: Describes the shape of user data in our app.
- RegisterDTO: Data Transfer Object for user registration requests.
- LoginDTO: Data Transfer Object for login requests.
- AuthResponse: The structure of the response when a user logs in or registers (e.g., token + user info).
Post types (src/types/post.types.ts
):
export interface Post {
id: number;
userId: number;
content: string;
createdAt: Date;
}
export interface CreatePostDTO {
content: string;
}
export interface PostResponse extends Omit<Post, 'userId'> {
username: string;
}
Explanation:
- Post interface: Represents a post’s data in the database.
- CreatePostDTO: The required payload for creating a new post.
- PostResponse: A custom interface for returning post data to the client. We omit the
userId
field (as it might be sensitive or unnecessary) and include theusername
.
Services
Authentication service (src/services/auth.service.ts
):
import { Pool } from 'pg';
import jwt from 'jsonwebtoken';
import { User, RegisterDTO, LoginDTO, AuthResponse } from '../types/auth.types';
import { hashPassword, verifyPassword } from '../utils/password';
import { CustomError } from '../middleware/error.middleware';
import env from '../config/environment';
export class AuthService {
constructor(private db: Pool) {}
async register(dto: RegisterDTO): Promise<AuthResponse> {
const hashedPassword = await hashPassword(dto.password);
const result = await this.db.query<User>(
`INSERT INTO users (username, email, password)
VALUES ($1, $2, $3)
RETURNING id, username, email`,
[dto.username, dto.email, hashedPassword]
);
const user = result.rows[0];
const token = this.generateToken(user.id);
return {
token,
user: {
id: user.id,
username: user.username,
email: user.email
}
};
}
async login(dto: LoginDTO): Promise<AuthResponse> {
const result = await this.db.query<User>(
'SELECT * FROM users WHERE email = $1',
[dto.email]
);
const user = result.rows[0];
if (!user) {
throw new CustomError('Invalid credentials', 401);
}
const validPassword = await verifyPassword(dto.password, user.password);
if (!validPassword) {
throw new CustomError('Invalid credentials', 401);
}
const token = this.generateToken(user.id);
return {
token,
user: {
id: user.id,
username: user.username,
email: user.email
}
};
}
private generateToken(userId: number): string {
return jwt.sign(
{ userId },
env.JWT_SECRET,
{ expiresIn: env.JWT_EXPIRES_IN }
);
}
}
Explanation:
- register method:
- Hashes the user’s password.
- Inserts the new user into the
users
table.- Returns a JWT token along with user details (ID, username, email).
- login method:
- Looks up a user by email.
- Verifies the password with
verifyPassword
.- Returns a JWT token if valid.
- generateToken: Creates a signed JWT token with the user’s ID and an expiration period.
Post service (src/services/post.service.ts
):
import { Pool } from 'pg';
import { Post, CreatePostDTO, PostResponse } from '../types/post.types';
import { CustomError } from '../middleware/error.middleware';
export class PostService {
constructor(private db: Pool) {}
async createPost(userId: number, dto: CreatePostDTO): Promise<PostResponse> {
const result = await this.db.query<Post & { username: string }>(
`INSERT INTO posts (user_id, content)
VALUES ($1, $2)
RETURNING posts.*, users.username
FROM posts
JOIN users ON posts.user_id = users.id`,
[userId, dto.content]
);
return this.mapPostToResponse(result.rows[0]);
}
async getPosts(): Promise<PostResponse[]> {
const result = await this.db.query<Post & { username: string }>(
`SELECT posts.*, users.username
FROM posts
JOIN users ON posts.user_id = users.id
ORDER BY posts.created_at DESC`
);
return result.rows.map(this.mapPostToResponse);
}
private mapPostToResponse(post: Post & { username: string }): PostResponse {
return {
id: post.id,
content: post.content,
createdAt: post.createdAt,
username: post.username
};
}
}
Explanation:
- createPost method: Inserts a post into the
posts
table, returning the newly created row joined with theusers
table to fetch the username.- getPosts method: Fetches all posts, ordered by
created_at
in descending order, and returns an array of PostResponse objects.- mapPostToResponse: A helper function to transform database records into a format suitable for the API response (excluding sensitive fields like
userId
).
Controllers
Auth controller (src/controllers/auth.controller.ts
):
import { Request, Response, NextFunction } from 'express';
import { AuthService } from '../services/auth.service';
import { RegisterDTO, LoginDTO } from '../types/auth.types';
export class AuthController {
constructor(private authService: AuthService) {}
register = async (
req: Request<{}, {}, RegisterDTO>,
res: Response,
next: NextFunction
) => {
try {
const result = await this.authService.register(req.body);
res.status(201).json(result);
} catch (error) {
next(error);
}
};
login = async (
req: Request<{}, {}, LoginDTO>,
res: Response,
next: NextFunction
) => {
try {
const result = await this.authService.login(req.body);
res.json(result);
} catch (error) {
next(error);
}
};
}
Explanation:
- AuthController: Defines the express route handlers for authentication.
- register: Handles incoming registration requests by calling the AuthService.
- login: Handles login requests; upon success, responds with a JWT token.
- next(error): Passes errors to the error-handling middleware.
Post controller (src/controllers/post.controller.ts
):
import { Request, Response, NextFunction } from 'express';
import { PostService } from '../services/post.service';
import { CreatePostDTO } from '../types/post.types';
export class PostController {
constructor(private postService: PostService) {}
createPost = async (
req: Request<{}, {}, CreatePostDTO>,
res: Response,
next: NextFunction
) => {
try {
const post = await this.postService.createPost(req.userId!, req.body);
res.status(201).json(post);
} catch (error) {
next(error);
}
};
getPosts = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const posts = await this.postService.getPosts();
res.json(posts);
} catch (error) {
next(error);
}
};
}
Explanation:
- PostController: Defines how to handle post-related operations (creation, retrieval).
- createPost: Uses
req.userId
to associate the newly created post with the authenticated user.- getPosts: Returns an array of all posts in descending order based on their creation time.
Middleware
Error handling (src/middleware/error.middleware.ts
):
import { Request, Response, NextFunction } from 'express';
import logger from '../utils/logger';
export class CustomError extends Error {
constructor(
message: string,
public statusCode: number
) {
super(message);
this.name = 'CustomError';
}
}
export const errorHandler = (
error: Error,
_req: Request,
res: Response,
_next: NextFunction
) => {
logger.error(error);
if (error instanceof CustomError) {
return res.status(error.statusCode).json({
error: error.message
});
}
return res.status(500).json({
error: 'Internal server error'
});
};
Explanation:
- CustomError: A specialized Error class that allows us to define a custom HTTP status code.
- errorHandler: A global error-handling middleware that logs errors and sends a unified response format to clients.
- If the error is a CustomError, we use its
statusCode
, otherwise we default to HTTP 500 (Internal Server Error).
Authentication middleware (src/middleware/auth.middleware.ts
):
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import env from '../config/environment';
import { CustomError } from './error.middleware';
interface JwtPayload {
userId: number;
}
declare global {
namespace Express {
interface Request {
userId?: number;
}
}
}
export const authenticateToken = (
req: Request,
_res: Response,
next: NextFunction
) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
throw new CustomError('Authentication required', 401);
}
const decoded = jwt.verify(token, env.JWT_SECRET) as JwtPayload;
req.userId = decoded.userId;
next();
} catch (error) {
next(new CustomError('Invalid token', 403));
}
};
Explanation:
- authenticateToken: Extracts the token from the Authorization header.
- Verifies the token using our JWT_SECRET. If verification passes, it attaches the
userId
to the request object so that downstream handlers know which user is making the request.- If the token is missing or invalid, it throws a CustomError.
Routes
Auth routes (src/routes/auth.routes.ts
):
import { Router } from 'express';
import { AuthController } from '../controllers/auth.controller';
import { AuthService } from '../services/auth.service';
import pool from '../config/database';
const router = Router();
const authService = new AuthService(pool);
const authController = new AuthController(authService);
router.post('/register', authController.register);
router.post('/login', authController.login);
export default router;
Explanation:
- Creates an Express Router instance.
- Instantiates the AuthService with our database connection pool.
- Instantiates the AuthController with the service, then maps
POST /register
andPOST /login
to their respective controller methods.
Post routes (src/routes/post.routes.ts
):
import { Router } from 'express';
import { PostController } from '../controllers/post.controller';
import { PostService } from '../services/post.service';
import { authenticateToken } from '../middleware/auth.middleware';
import pool from '../config/database';
const router = Router();
const postService = new PostService(pool);
const postController = new PostController(postService);
router.get('/', postController.getPosts);
router.post('/', authenticateToken, postController.createPost);
export default router;
Explanation:
- Creates an Express Router instance for post-related endpoints.
- GET /api/posts: Accessible without authentication to read posts.
- POST /api/posts: Protected by
authenticateToken
, requiring a valid JWT before creating a post.
Utilities
Logger utility (src/utils/logger.ts
):
import winston from 'winston';
import env from '../config/environment';
const logger = winston.createLogger({
level: env.NODE_ENV === 'production' ? 'info' : 'debug',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});
export default logger;
Explanation:
- Winston: A popular logging library providing different logging levels and output formats.
- In development, logging level is set to
debug
for more verbosity; in production, it’sinfo
to reduce noise.- Logs can be output to the console or other transports (e.g., files, remote logging services).
Password utilities (src/utils/password.ts
):
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
export const hashPassword = (password: string): Promise<string> => {
return bcrypt.hash(password, SALT_ROUNDS);
};
export const verifyPassword = (
password: string,
hashedPassword: string
): Promise<boolean> => {
return bcrypt.compare(password, hashedPassword);
};
Explanation:
- bcrypt: A library to securely hash and compare passwords.
- SALT_ROUNDS: A parameter that influences the computational cost of hashing; higher values are more secure but take more time.
- hashPassword: Transforms a plain-text password into a secure hash.
- verifyPassword: Checks if the provided plain-text password matches the previously hashed one.
Main Application
Main application file (src/app.ts
):
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import env from './config/environment';
import { errorHandler } from './middleware/error.middleware';
import authRoutes from './routes/auth.routes';
import postRoutes from './routes/post.routes';
import logger from './utils/logger';
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({
origin: env.CORS_ORIGIN,
credentials: true
}));
app.use(rateLimit({
windowMs: env.RATE_LIMIT_WINDOW_MS,
max: env.RATE_LIMIT_MAX_REQUESTS,
message: { error: 'Too many requests, please try again later.' }
}));
// Body parsing
app.use(express.json());
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/posts', postRoutes);
// Error handling
app.use(errorHandler);
const start = async () => {
try {
app.listen(env.PORT, () => {
logger.info(`Server started on port ${env.PORT}`);
});
} catch (error) {
logger.error('Failed to start server', error);
process.exit(1);
}
};
start();
Explanation:
- helmet: Adds security headers to the response (e.g., HSTS, X-Content-Type-Options).
- cors: Controls which origins can send requests to the server.
- express-rate-limit: Limits the number of requests from a single IP in a given period (e.g., 100 requests per 15 minutes), helping prevent brute force or DDoS attacks.
- express.json(): Parses JSON bodies in incoming requests.
- app.use(…): Mounts middleware in the Express pipeline.
- Routes: Organized under
/api/auth
and/api/posts
.- errorHandler: Registered last to catch any unhandled errors.
Authentication & Security
Security Best Practices
- Password Security:
- Passwords are hashed using bcrypt with a cost factor of 12
- Original passwords are never stored or logged
- Password strength requirements should be enforced on the frontend
- JWT Security:
- Tokens expire after a configurable time (default: 1 hour)
- Secret key is at least 32 characters long
- Tokens are verified on every protected route
- API Security:
- Helmet.js provides security headers
- Rate limiting prevents brute force attacks
- CORS is configured for specific origins
- Input validation prevents injection attacks
- Database Security:
- Parameterized queries prevent SQL injection
- SSL enabled in production
- Proper indexing for performance
- Cascading deletes maintain referential integrity
Explanation:
- These are essential guidelines to keep your application secure.
- Hashing ensures stolen passwords can’t be easily reversed.
- JWT secrets must remain confidential.
- Helmet, CORS, and rate-limit are widely recommended Node.js security measures.
API Testing
Example requests using curl:
# Register a new user
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"testuser","email":"test@example.com","password":"Password123!"}'
# Login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"Password123!"}'
# Create a post (replace TOKEN with your JWT)
curl -X POST http://localhost:3000/api/posts \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"content":"Hello, World!"}'
# Get all posts
curl http://localhost:3000/api/posts
Explanation:
- curl commands illustrate how to test each endpoint.
- Replace
TOKEN
with the JWT you get from the login response to access protected routes.
Best Practices
Code Organization
- Use TypeScript for type safety
- Implement service layer pattern
- Follow single responsibility principle
- Use dependency injection
- Maintain consistent error handling
Performance
- Use database indexes
- Implement rate limiting
- Enable compression for responses
- Use connection pooling
- Cache when appropriate
Security
- Validate all input
- Use security headers
- Rate limit sensitive endpoints
- Follow least privilege principle
- Keep dependencies updated
Logging
- Use structured logging
- Log appropriate detail level
- Don’t log sensitive data
- Include request IDs
- Monitor errors
Explanation:
- Following these best practices ensures your code remains maintainable, efficient, and secure.
- Code Organization: A clean structure and separation of concerns prevent large “God” files.
- Performance: Database indexing, caching, rate-limiting, and pooling are typical ways to ensure responsive endpoints.
- Security: Keep an eye on new vulnerabilities; update dependencies regularly.
- Logging: Helps debug issues and monitor server health in production.
Next Steps
Feature Enhancements
- Implement refresh tokens
- Add email verification
- Create password reset flow
- Add user profiles
- Implement file uploads
Technical Improvements
- Add automated tests
- Set up CI/CD pipeline
- Implement caching
- Add database migrations
- Set up monitoring
Documentation
- Create API documentation
- Add JSDoc comments
- Create deployment guide
- Document error codes
- Add contribution guidelines
Explanation:
- Feature Enhancements: More robust user identity handling (refresh tokens, email verification) and user-generated content (profiles, uploads).
- Technical Improvements: Ensure quality and maintainability using automated tests, CI/CD, and migrations to handle database schema changes.
- Documentation: Crucial for sharing knowledge and clarifying API usage to other developers or external teams.
Additional Resources
Official Documentation
Security Resources
Tools
Explanation:
- Official Documentation: Primary sources for detailed references and examples.
- Security Resources: Guides on common vulnerabilities and how to avoid them.
- Tools: Recommended for productivity and debugging.
Running in Production
Deployment Checklist
- Environment Configuration
- Set NODE_ENV to ‘production’
- Use secure database credentials
- Configure proper CORS origins
- Set appropriate rate limits
- Security
- Enable SSL/TLS
- Set secure HTTP headers
- Configure proper firewall rules
- Enable database encryption
- Monitoring
- Set up error tracking
- Configure performance monitoring
- Enable request logging
- Set up alerts
- Scaling
- Use load balancer
- Configure connection pooling
- Enable caching
- Optimize queries
Explanation:
- In production, you typically tighten security, enable SSL/TLS to encrypt traffic, and handle higher volumes of traffic efficiently.
- Monitoring helps you detect issues quickly, while scaling strategies ensure your server can handle growth.
Example Production Configuration
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:pass@host:5432/dbname?ssl=true
JWT_SECRET=your-secure-production-secret
JWT_EXPIRES_IN=1h
CORS_ORIGIN=https://your-frontend-domain.com
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
Explanation:
- Ensures the database connection is secured with
ssl=true
.- Uses an appropriately secure JWT secret.
- Limits CORS to your production frontend domain.
Conclusion
🎉 Congratulations! You now have a modern, secure, and well-structured backend API. This implementation follows best practices and provides a solid foundation for building real-world applications.
Remember to:
- Keep dependencies updated
- Regularly review security practices
- Monitor application performance
- Write tests for new features
- Document API changes
Happy coding! 🚀