
Secure API Implementation Guide for Vibe Coding
Introduction
APIs (Application Programming Interfaces) serve as the critical communication channels between different components of modern applications, enabling data exchange, integration with third-party services, and separation of frontend and backend concerns. In the rapidly evolving landscape of vibe coding, where AI tools generate code based on natural language prompts, API security often receives inadequate attention, leading to significant vulnerabilities that malicious actors can exploit to compromise data integrity, confidentiality, and system availability.
According to a recent report by Salt Security, API attacks increased by 681% in 2022 alone, with 94% of organizations experiencing some form of API security incident. This alarming trend highlights the critical importance of implementing secure API practices in all applications, especially those built with vibe coding tools where security considerations might be overlooked in favor of rapid development.
When developers instruct AI to “create an API endpoint for user data” or “build a payment processing API,” the resulting code often implements basic functionality without incorporating crucial security measures like proper authentication, input validation, rate limiting, or encryption. These omissions create significant security gaps that can lead to data breaches, unauthorized access, and service disruptions.
This article provides a comprehensive guide to implementing secure APIs in vibe-coded applications built with popular full-stack app builders like Lovable.dev, Bolt.new, Tempo Labs, Base44, and Replit. We’ll examine common API security vulnerabilities in AI-generated code, demonstrate secure implementation approaches for each platform, and provide practical techniques for enhancing API security. By following these practices, you can ensure that your vibe-coded applications maintain robust protection against API-related threats while still benefiting from the rapid development that AI tools enable.
Common API Security Vulnerabilities in AI-Generated Code
AI coding assistants typically follow certain patterns when generating API endpoints. Understanding these patterns and their associated vulnerabilities is the first step toward implementing secure APIs.
Insufficient Authentication and Authorization
One of the most common and dangerous vulnerabilities in AI-generated API code is inadequate authentication and authorization:
// Example of insecure API endpoint in AI-generated code
app.get('/api/users', (req, res) => {
// No authentication check
// No authorization verification
// Fetch all users from database
db.query('SELECT * FROM users', (err, results) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
// Return sensitive user data
res.json(results);
});
});
This approach creates several security risks:
- Unauthenticated access to sensitive user data
- No verification of user permissions or roles
- No protection against unauthorized data access
- Potential exposure of sensitive personal information
Lack of Input Validation
AI-generated API endpoints often lack proper input validation:
// Example of API endpoint without input validation
app.post('/api/products', (req, res) => {
const { name, price, description, category } = req.body;
// No validation of inputs
// No sanitization of data
// Insert product into database
db.query(
'INSERT INTO products (name, price, description, category) VALUES (?, ?, ?, ?)',
[name, price, description, category],
(err, result) => {
if (err) {
return res.status(500).json({ error: 'Failed to create product' });
}
res.status(201).json({ id: result.insertId, message: 'Product created' });
}
);
});
This code is vulnerable because:
- No validation of data types or formats
- No checks for required fields
- No sanitization of inputs
- No protection against malicious payloads
Missing Rate Limiting
AI tools rarely include rate limiting in generated API code:
// Example of API endpoint without rate limiting
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
try {
// Find user by email
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Check password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Generate token
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, {
expiresIn: '1h'
});
res.json({ token });
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
This endpoint is vulnerable to:
- Brute force attacks
- Credential stuffing attacks
- Denial of service (DoS) attacks
- Resource exhaustion
Insecure Error Handling
AI-generated code often includes insecure error handling that leaks sensitive information:
// Example of insecure error handling
app.get('/api/user/:id', (req, res) => {
const userId = req.params.id;
db.query('SELECT * FROM users WHERE id = ?', [userId], (err, results) => {
if (err) {
// Vulnerable: Returning detailed error information
return res.status(500).json({
error: 'Database error',
details: err.message,
stack: err.stack,
query: err.sql
});
}
if (results.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.json(results[0]);
});
});
This approach leaks sensitive information:
- Database error messages that reveal implementation details
- Stack traces that expose code structure
- SQL queries that reveal database schema
- Information that could aid attackers in exploiting vulnerabilities
Lack of HTTPS Enforcement
AI tools often generate code without HTTPS enforcement:
// Example of server setup without HTTPS enforcement
const express = require('express');
const app = express();
// Middleware setup
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// API routes
app.use('/api', apiRoutes);
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
This code is problematic because:
- No redirection from HTTP to HTTPS
- No Strict-Transport-Security header
- Data transmitted in plaintext could be intercepted
- Vulnerable to man-in-the-middle attacks
Missing CORS Configuration
AI-generated API code often lacks proper CORS (Cross-Origin Resource Sharing) configuration:
// Example of missing or overly permissive CORS configuration
const express = require('express');
const cors = require('cors');
const app = express();
// Vulnerable: Allowing all origins
app.use(cors());
// API routes
app.use('/api', apiRoutes);
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
This configuration is insecure because:
- It allows requests from any origin
- No restrictions on methods or headers
- Increases vulnerability to cross-site request forgery (CSRF)
- Enables malicious sites to interact with your API
Platform-Specific API Security Vulnerabilities
Each full-stack app builder has its own patterns of API implementation. Let’s examine specific examples from each platform.
Lovable.dev
Lovable.dev integrates with Supabase for backend functionality, but AI-generated code might not implement proper security measures:
// Lovable.dev vulnerable API implementation
import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs';
// API route for fetching user data
export default async function handler(req, res) {
// Create Supabase client
const supabase = createServerSupabaseClient({ req, res });
// Get user data
const { data: userData, error: userError } = await supabase
.from('profiles')
.select('*');
if (userError) {
// Vulnerable: Returning detailed error information
return res.status(500).json({ error: userError });
}
// Vulnerable: No filtering of sensitive data
return res.status(200).json(userData);
}
The issues here include:
- No proper authorization checks for data access
- Returning all user profiles regardless of the requester’s role
- Exposing detailed error information
- No rate limiting to prevent abuse
Bolt.new
Bolt.new might generate Next.js API routes with security vulnerabilities:
// Bolt.new vulnerable API implementation
import type { NextApiRequest, NextApiResponse } from 'next';
import { supabase } from '../../lib/supabaseClient';
type Data = {
products: any[];
} | {
error: string;
details?: any;
};
// API route for product filtering
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
// Vulnerable: No authentication check
const { category, minPrice, maxPrice } = req.query;
try {
let query = supabase.from('products').select('*');
// Apply filters
if (category) {
// Vulnerable: No validation of category
query = query.eq('category', category);
}
if (minPrice) {
// Vulnerable: No validation of minPrice
query = query.gte('price', minPrice);
}
if (maxPrice) {
// Vulnerable: No validation of maxPrice
query = query.lte('price', maxPrice);
}
const { data, error } = await query;
if (error) {
// Vulnerable: Returning detailed error information
return res.status(500).json({ error: 'Database error', details: error });
}
return res.status(200).json({ products: data });
} catch (error) {
// Vulnerable: Returning detailed error information
return res.status(500).json({ error: 'Server error', details: error });
}
}
The vulnerabilities include:
- No authentication or authorization checks
- No input validation for query parameters
- Exposing detailed error information
- No rate limiting to prevent abuse
Tempo Labs
Tempo Labs might generate API code with insufficient security measures:
// Tempo Labs vulnerable API implementation
import { PrismaClient } from '@prisma/client';
import { verify } from 'jsonwebtoken';
const prisma = new PrismaClient();
// API route for user operations
export default async function handler(req, res) {
// Basic authentication check
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}
const token = authHeader.split(' ')[1];
try {
// Verify token
const decoded = verify(token, process.env.JWT_SECRET);
req.user = decoded;
// Handle different HTTP methods
switch (req.method) {
case 'GET':
return getUserData(req, res);
case 'PUT':
return updateUserData(req, res);
case 'DELETE':
return deleteUser(req, res);
default:
return res.status(405).json({ error: 'Method not allowed' });
}
} catch (error) {
// Vulnerable: Returning token verification errors
return res.status(401).json({ error: 'Invalid token', details: error.message });
}
}
// Get user data
async function getUserData(req, res) {
try {
// Vulnerable: No role-based access control
const users = await prisma.user.findMany({
include: {
profile: true,
orders: true
}
});
// Vulnerable: Returning all user data
return res.status(200).json(users);
} catch (error) {
// Vulnerable: Returning detailed error information
return res.status(500).json({ error: 'Database error', details: error.message });
}
}
// Update user data
async function updateUserData(req, res) {
const { id, ...data } = req.body;
// Vulnerable: No validation of input data
try {
const updatedUser = await prisma.user.update({
where: { id },
data
});
return res.status(200).json(updatedUser);
} catch (error) {
// Vulnerable: Returning detailed error information
return res.status(500).json({ error: 'Update failed', details: error.message });
}
}
// Delete user
async function deleteUser(req, res) {
const { id } = req.body;
// Vulnerable: No additional authorization check for deletion
try {
await prisma.user.delete({
where: { id }
});
return res.status(200).json({ message: 'User deleted' });
} catch (error) {
// Vulnerable: Returning detailed error information
return res.status(500).json({ error: 'Deletion failed', details: error.message });
}
}
The security issues include:
- No role-based access control
- No validation of input data
- Returning all user data regardless of the requester’s role
- Exposing detailed error information
- No rate limiting to prevent abuse
Base44
Base44 often generates more bare-bones API code with significant security gaps:
// Base44 vulnerable API implementation
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// Middleware to verify token
function verifyToken(req, res, next) {
const token = req.header('auth-token');
if (!token) return res.status(401).json({ error: 'Access denied' });
try {
const verified = jwt.verify(token, 'your_jwt_secret');
req.user = verified;
next();
} catch (error) {
res.status(400).json({ error: 'Invalid token' });
}
}
// Get all users
router.get('/users', verifyToken, async (req, res) => {
// Vulnerable: No role check for accessing all users
try {
const users = await User.find();
res.json(users);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get user by ID
router.get('/users/:id', verifyToken, async (req, res) => {
// Vulnerable: No check if user is requesting their own data
try {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Create new user
router.post('/users', async (req, res) => {
// Vulnerable: No input validation
const user = new User({
name: req.body.name,
email: req.body.email,
password: req.body.password
});
try {
const savedUser = await user.save();
res.status(201).json(savedUser);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Update user
router.put('/users/:id', verifyToken, async (req, res) => {
// Vulnerable: No check if user is updating their own data
// Vulnerable: No input validation
try {
const updatedUser = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true }
);
if (!updatedUser) return res.status(404).json({ error: 'User not found' });
res.json(updatedUser);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Delete user
router.delete('/users/:id', verifyToken, async (req, res) => {
// Vulnerable: No check if user is deleting their own data or has admin rights
try {
const removedUser = await User.findByIdAndDelete(req.params.id);
if (!removedUser) return res.status(404).json({ error: 'User not found' });
res.json({ message: 'User deleted' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
The security issues include:
- Hardcoded JWT secret
- No role-based access control
- No input validation
- No rate limiting
- Exposing detailed error messages
Replit
Replit’s AI assistant often generates Python API code with security weaknesses:
# Replit vulnerable API implementation
from flask import Flask, request, jsonify
import jwt
from functools import wraps
import sqlite3
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'
# Database connection
def get_db_connection():
conn = sqlite3.connect('database.db')
conn.row_factory = sqlite3.Row
return conn
# Token verification decorator
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'error': 'Token is missing'}), 401
try:
# Vulnerable: Using a hardcoded secret key
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
# Vulnerable: Not checking if the user exists or is active
except:
return jsonify({'error': 'Token is invalid'}), 401
return f(*args, **kwargs)
return decorated
# Get all users
@app.route('/api/users', methods=['GET'])
@token_required
def get_users():
# Vulnerable: No role check for accessing all users
conn = get_db_connection()
users = conn.execute('SELECT * FROM users').fetchall()
conn.close()
# Vulnerable: Converting all user data including passwords
return jsonify([dict(user) for user in users])
# Get user by ID
@app.route('/api/users/<int:user_id>', methods=['GET'])
@token_required
def get_user(user_id):
# Vulnerable: No check if user is requesting their own data
conn = get_db_connection()
user = conn.execute('SELECT * FROM users WHERE id = ?', (user_id,)).fetchone()
conn.close()
if not user:
return jsonify({'error': 'User not found'}), 404
# Vulnerable: Returning all user data including password
return jsonify(dict(user))
# Create new user
@app.route('/api/users', methods=['POST'])
def create_user():
# Vulnerable: No input validation
data = request.get_json()
if not data or not 'email' in data or not 'password' in data:
return jsonify({'error': 'Missing required fields'}), 400
conn = get_db_connection()
# Vulnerable: Storing password without hashing
conn.execute(
'INSERT INTO users (name, email, password) VALUES (?, ?, ?)',
(data.get('name'), data['email'], data['password'])
)
conn.commit()
user_id = conn.execute('SELECT last_insert_rowid()').fetchone()[0]
conn.close()
return jsonify({'id': user_id, 'message': 'User created successfully'}), 201
# Update user
@app.route('/api/users/<int:user_id>', methods=['PUT'])
@token_required
def update_user(user_id):
# Vulnerable: No check if user is updating their own data
# Vulnerable: No input validation
data = request.get_json()
conn = get_db_connection()
user = conn.execute('SELECT * FROM users WHERE id = ?', (user_id,)).fetchone()
if not user:
conn.close()
return jsonify({'error': 'User not found'}), 404
# Vulnerable: Updating all fields without validation
name = data.get('name', user['name'])
email = data.get('email', user['email'])
password = data.get('password', user['password'])
conn.execute(
'UPDATE users SET name = ?, email = ?, password = ? WHERE id = ?',
(name, email, password, user_id)
)
conn.commit()
conn.close()
return jsonify({'message': 'User updated successfully'})
# Delete user
@app.route('/api/users/<int:user_id>', methods=['DELETE'])
@token_required
def delete_user(user_id):
# Vulnerable: No check if user is deleting their own data or has admin rights
conn = get_db_connection()
user = conn.execute('SELECT * FROM users WHERE id = ?', (user_id,)).fetchone()
if not user:
conn.close()
return jsonify({'error': 'User not found'}), 404
conn.execute('DELETE FROM users WHERE id = ?', (user_id,))
conn.commit()
conn.close()
return jsonify({'message': 'User deleted successfully'})
if __name__ == '__main__':
app.run(debug=True)
The security issues include:
- Hardcoded secret key
- Storing passwords without hashing
- No role-based access control
- No input validation
- Exposing sensitive user data
- Running with debug mode enabled
Secure API Implementation Techniques
Now that we’ve identified common vulnerabilities, let’s explore secure implementation techniques for APIs in vibe-coded applications.
Proper Authentication and Authorization
Implement robust authentication and authorization mechanisms:
// Secure authentication middleware
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.user = user;
next();
});
}
// Role-based authorization middleware
function authorizeRole(roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Secure API endpoint with authentication and authorization
app.get('/api/users',
authenticateToken,
authorizeRole(['admin']),
(req, res) => {
// Only admins can access this endpoint
db.query('SELECT id, username, email FROM users', (err, results) => {
if (err) {
return res.status(500).json({ error: 'Internal server error' });
}
res.json(results);
});
}
);
// Endpoint with owner-based authorization
app.get('/api/users/:id',
authenticateToken,
(req, res) => {
const userId = req.params.id;
// Check if user is requesting their own data or is an admin
if (req.user.id !== userId && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Insufficient permissions' });
}
db.query('SELECT id, username, email FROM users WHERE id = ?',
[userId],
(err, results) => {
if (err) {
return res.status(500).json({ error: 'Internal server error' });
}
if (results.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.json(results[0]);
}
);
}
);
This approach:
- Verifies the authenticity of requests using JWT
- Implements role-based access control
- Enforces owner-based authorization for personal data
- Prevents unauthorized access to sensitive information
Comprehensive Input Validation
Implement thorough input validation for all API endpoints:
// Secure input validation with express-validator
const { body, param, query, validationResult } = require('express-validator');
// Create product endpoint with validation
app.post('/api/products',
authenticateToken,
authorizeRole(['admin', 'editor']),
[
// Validate request body
body('name')
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('Product name must be between 2 and 100 characters'),
body('price')
.isNumeric()
.withMessage('Price must be a number')
.custom(value => value >= 0)
.withMessage('Price cannot be negative'),
body('description')
.trim()
.isLength({ max: 1000 })
.withMessage('Description cannot exceed 1000 characters'),
body('category')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Category must be between 2 and 50 characters')
],
(req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { name, price, description, category } = req.body;
// Insert validated product into database
db.query(
'INSERT INTO products (name, price, description, category) VALUES (?, ?, ?, ?)',
[name, price, description, category],
(err, result) => {
if (err) {
return res.status(500).json({ error: 'Internal server error' });
}
res.status(201).json({
id: result.insertId,
message: 'Product created successfully'
});
}
);
}
);
// Search products endpoint with validation
app.get('/api/products',
[
// Validate query parameters
query('category')
.optional()
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Invalid category'),
query('minPrice')
.optional()
.isNumeric()
.withMessage('Min price must be a number')
.custom(value => value >= 0)
.withMessage('Min price cannot be negative'),
query('maxPrice')
.optional()
.isNumeric()
.withMessage('Max price must be a number')
.custom(value => value >= 0)
.withMessage('Max price cannot be negative'),
query('page')
.optional()
.isInt({ min: 1 })
.withMessage('Page must be a positive integer'),
query('limit')
.optional()
.isInt({ min: 1, max: 100 })
.withMessage('Limit must be between 1 and 100')
],
(req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process validated query parameters
const category = req.query.category;
const minPrice = req.query.minPrice ? parseFloat(req.query.minPrice) : null;
const maxPrice = req.query.maxPrice ? parseFloat(req.query.maxPrice) : null;
const page = req.query.page ? parseInt(req.query.page) : 1;
const limit = req.query.limit ? parseInt(req.query.limit) : 20;
const offset = (page - 1) * limit;
// Build query with validated parameters
let query = 'SELECT * FROM products WHERE 1=1';
const params = [];
if (category) {
query += ' AND category = ?';
params.push(category);
}
if (minPrice !== null) {
query += ' AND price >= ?';
params.push(minPrice);
}
if (maxPrice !== null) {
query += ' AND price <= ?';
params.push(maxPrice);
}
query += ' LIMIT ? OFFSET ?';
params.push(limit, offset);
// Execute query with validated parameters
db.query(query, params, (err, results) => {
if (err) {
return res.status(500).json({ error: 'Internal server error' });
}
res.json(results);
});
}
);
This approach:
- Validates all input data before processing
- Enforces data type, format, and range constraints
- Provides clear error messages for invalid inputs
- Prevents injection attacks and data corruption
Implement Rate Limiting
Add rate limiting to protect against abuse:
// Secure rate limiting implementation
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
const redis = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
});
// General API rate limiter
const apiLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redis.call(...args)
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
message: { error: 'Too many requests, please try again later' }
});
// More strict rate limiter for authentication endpoints
const authLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redis.call(...args)
}),
windowMs: 60 * 60 * 1000, // 1 hour
max: 10, // Limit each IP to 10 requests per windowMs
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many login attempts, please try again later' }
});
// Apply rate limiting to routes
app.use('/api/', apiLimiter); // Apply to all API routes
app.use('/api/login', authLimiter); // Apply stricter limits to login
app.use('/api/register', authLimiter); // Apply stricter limits to registration
// Dynamic rate limiting based on user role
function dynamicRateLimit(req, res, next) {
const userRole = req.user ? req.user.role : 'anonymous';
// Set limits based on user role
let limit;
switch (userRole) {
case 'admin':
limit = 1000;
break;
case 'premium':
limit = 500;
break;
case 'user':
limit = 100;
break;
default:
limit = 50;
}
// Create and apply dynamic limiter
const dynamicLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
prefix: `ratelimit:${userRole}:`
}),
windowMs: 15 * 60 * 1000,
max: limit,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Rate limit exceeded' }
});
return dynamicLimiter(req, res, next);
}
// Apply dynamic rate limiting to specific routes
app.use('/api/data', authenticateToken, dynamicRateLimit);
This approach:
- Limits request frequency to prevent abuse
- Applies stricter limits to sensitive endpoints
- Uses Redis for distributed rate limiting
- Implements dynamic limits based on user roles
- Provides clear feedback when limits are exceeded
Secure Error Handling
Implement secure error handling that doesn’t leak sensitive information:
// Secure error handling middleware
function errorHandler(err, req, res, next) {
// Log the error for internal debugging
console.error('Error:', err);
// Determine error type and set appropriate status code
let statusCode = 500;
let errorMessage = 'Internal server error';
if (err.name === 'ValidationError') {
statusCode = 400;
errorMessage = 'Invalid input data';
} else if (err.name === 'UnauthorizedError') {
statusCode = 401;
errorMessage = 'Authentication required';
} else if (err.name === 'ForbiddenError') {
statusCode = 403;
errorMessage = 'Insufficient permissions';
} else if (err.name === 'NotFoundError') {
statusCode = 404;
errorMessage = 'Resource not found';
}
// Send generic error response without sensitive details
res.status(statusCode).json({ error: errorMessage });
}
// Custom error classes
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
class UnauthorizedError extends Error {
constructor(message) {
super(message);
this.name = 'UnauthorizedError';
}
}
class ForbiddenError extends Error {
constructor(message) {
super(message);
this.name = 'ForbiddenError';
}
}
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = 'NotFoundError';
}
}
// Apply error handling middleware
app.use(errorHandler);
// Example API endpoint with secure error handling
app.get('/api/users/:id', authenticateToken, async (req, res, next) => {
try {
const userId = req.params.id;
// Check if user is requesting their own data or is an admin
if (req.user.id !== userId && req.user.role !== 'admin') {
throw new ForbiddenError('Cannot access other user data');
}
const user = await User.findById(userId);
if (!user) {
throw new NotFoundError('User not found');
}
// Return only necessary user data
res.json({
id: user.id,
username: user.username,
email: user.email,
createdAt: user.createdAt
});
} catch (error) {
// Pass error to error handling middleware
next(error);
}
});
This approach:
- Provides generic error messages to clients
- Logs detailed errors for internal debugging
- Uses appropriate HTTP status codes
- Prevents leakage of sensitive information
- Maintains a consistent error response format
Enforce HTTPS
Ensure all API traffic uses HTTPS:
// Secure HTTPS enforcement middleware
function requireHTTPS(req, res, next) {
// The 'x-forwarded-proto' header is used by load balancers
if (
!req.secure &&
req.get('x-forwarded-proto') !== 'https' &&
process.env.NODE_ENV !== 'development'
) {
return res.redirect('https://' + req.get('host') + req.url);
}
next();
}
// Apply HTTPS middleware
app.use(requireHTTPS);
// Set security headers
app.use((req, res, next) => {
// Strict Transport Security
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
// Other security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
next();
});
This approach:
- Redirects HTTP requests to HTTPS
- Sets Strict-Transport-Security header to enforce HTTPS
- Adds additional security headers
- Prevents man-in-the-middle attacks
- Ensures data is transmitted securely
Proper CORS Configuration
Implement secure CORS configuration:
// Secure CORS configuration
const cors = require('cors');
// Define allowed origins
const allowedOrigins = [
'https://yourapplication.com',
'https://admin.yourapplication.com',
'https://api.yourapplication.com'
];
// Configure CORS options
const corsOptions = {
origin: function (origin, callback) {
// Allow requests with no origin (like mobile apps, curl, etc.)
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // 24 hours
};
// Apply CORS middleware
app.use(cors(corsOptions));
// Handle CORS errors
app.use((err, req, res, next) => {
if (err.message === 'Not allowed by CORS') {
return res.status(403).json({ error: 'Cross-origin request not allowed' });
}
next(err);
});
This approach:
- Restricts access to specific origins
- Limits allowed HTTP methods
- Specifies allowed headers
- Enables credentials for authenticated requests
- Caches preflight requests to improve performance
Platform-Specific Secure API Implementations
Let’s look at secure implementations for each full-stack app builder.
Lovable.dev Secure Implementation
For Lovable.dev’s Supabase-based applications:
// Secure Lovable.dev API implementation
import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs';
// API route for fetching user data
export default async function handler(req, res) {
try {
// Create Supabase client with auth context
const supabase = createServerSupabaseClient({ req, res });
// Check if user is authenticated
const { data: { session }, error: authError } = await supabase.auth.getSession();
if (authError || !session) {
return res.status(401).json({ error: 'Authentication required' });
}
// Get current user's profile
const { data: profile, error: profileError } = await supabase
.from('profiles')
.select('id, username, avatar_url, created_at')
.eq('id', session.user.id)
.single();
if (profileError) {
return res.status(500).json({ error: 'Failed to fetch profile' });
}
// Check if user is an admin for additional data
const { data: roleData, error: roleError } = await supabase
.from('user_roles')
.select('role')
.eq('user_id', session.user.id)
.single();
if (roleError && roleError.code !== 'PGRST116') { // Not found error
return res.status(500).json({ error: 'Failed to fetch user role' });
}
const isAdmin = roleData?.role === 'admin';
// If admin, fetch all profiles with limited fields
if (isAdmin && req.query.all === 'true') {
const { data: allProfiles, error: allProfilesError } = await supabase
.from('profiles')
.select('id, username, created_at')
.order('created_at', { ascending: false });
if (allProfilesError) {
return res.status(500).json({ error: 'Failed to fetch profiles' });
}
return res.status(200).json({
currentUser: profile,
isAdmin,
allProfiles
});
}
// Regular user only gets their own profile
return res.status(200).json({
currentUser: profile,
isAdmin
});
} catch (error) {
console.error('API error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}
This implementation:
- Uses Supabase’s authentication system properly
- Implements role-based access control
- Returns only necessary data fields
- Provides generic error messages
- Handles errors gracefully
Bolt.new Secure Implementation
For Bolt.new’s Next.js applications:
// Secure Bolt.new API implementation
import type { NextApiRequest, NextApiResponse } from 'next';
import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs';
import { z } from 'zod';
// Define response types
type SuccessResponse = {
products: Product[];
pagination: {
page: number;
limit: number;
total: number;
pages: number;
};
};
type ErrorResponse = {
error: string;
};
// Define product schema for validation
const QuerySchema = z.object({
category: z.string().min(2).max(50).optional(),
minPrice: z.coerce.number().min(0).optional(),
maxPrice: z.coerce.number().min(0).optional(),
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20)
});
// Rate limiting (simplified example - in production use Redis)
const rateLimits = new Map<string, { count: number, timestamp: number }>();
function checkRateLimit(ip: string, limit: number, windowMs: number): boolean {
const now = Date.now();
const record = rateLimits.get(ip);
if (!record) {
rateLimits.set(ip, { count: 1, timestamp: now });
return true;
}
if (now - record.timestamp > windowMs) {
// Reset if window has passed
rateLimits.set(ip, { count: 1, timestamp: now });
return true;
}
if (record.count >= limit) {
return false; // Rate limit exceeded
}
// Increment count
record.count += 1;
rateLimits.set(ip, record);
return true;
}
// API route for product filtering
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<SuccessResponse | ErrorResponse>
) {
// Check rate limit (20 requests per minute)
const clientIp = req.headers['x-forwarded-for'] as string || 'unknown';
if (!checkRateLimit(clientIp, 20, 60 * 1000)) {
return res.status(429).json({ error: 'Too many requests, please try again later' });
}
// Only allow GET method
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
// Create Supabase client
const supabase = createServerSupabaseClient({ req, res });
// Validate query parameters
const validationResult = QuerySchema.safeParse(req.query);
if (!validationResult.success) {
return res.status(400).json({
error: 'Invalid query parameters'
});
}
const { category, minPrice, maxPrice, page, limit } = validationResult.data;
const offset = (page - 1) * limit;
// Build query
let query = supabase
.from('products')
.select('id, name, description, price, category, created_at', { count: 'exact' });
// Apply filters
if (category) {
query = query.eq('category', category);
}
if (minPrice !== undefined) {
query = query.gte('price', minPrice);
}
if (maxPrice !== undefined) {
query = query.lte('price', maxPrice);
}
// Apply pagination
query = query.range(offset, offset + limit - 1).order('created_at', { ascending: false });
// Execute query
const { data, error, count } = await query;
if (error) {
console.error('Database error:', error);
return res.status(500).json({ error: 'Failed to fetch products' });
}
// Calculate pagination info
const total = count || 0;
const pages = Math.ceil(total / limit);
return res.status(200).json({
products: data || [],
pagination: {
page,
limit,
total,
pages
}
});
} catch (error) {
console.error('API error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}
This implementation:
- Uses Zod for strong input validation
- Implements rate limiting
- Provides proper pagination
- Returns only necessary data fields
- Handles errors gracefully
Tempo Labs Secure Implementation
For Tempo Labs’ applications:
// Secure Tempo Labs API implementation
import { PrismaClient } from '@prisma/client';
import { verify, sign } from 'jsonwebtoken';
import { hash, compare } from 'bcrypt';
import { z } from 'zod';
import rateLimit from 'express-rate-limit';
const prisma = new PrismaClient();
// Rate limiting middleware
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please try again later' }
});
// Authentication middleware
async function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authentication required' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = verify(token, process.env.JWT_SECRET);
// Check if user exists and is active
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: { id: true, role: true, active: true }
});
if (!user || !user.active) {
return res.status(401).json({ error: 'Invalid or inactive user' });
}
req.user = {
id: user.id,
role: user.role
};
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
// Authorization middleware
function authorize(roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Input validation schemas
const UserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
password: z.string().min(8).max(100)
});
const LoginSchema = z.object({
email: z.string().email(),
password: z.string()
});
// API route for user registration
export default async function handler(req, res) {
// Apply rate limiting
await apiLimiter(req, res, () => {});
if (req.method === 'POST' && req.url === '/api/register') {
try {
// Validate input
const validationResult = UserSchema.safeParse(req.body);
if (!validationResult.success) {
return res.status(400).json({
error: 'Invalid input data',
details: validationResult.error.format()
});
}
const { name, email, password } = validationResult.data;
// Check if user already exists
const existingUser = await prisma.user.findUnique({
where: { email }
});
if (existingUser) {
return res.status(409).json({ error: 'Email already in use' });
}
// Hash password
const hashedPassword = await hash(password, 12);
// Create user
const user = await prisma.user.create({
data: {
name,
email,
password: hashedPassword,
role: 'user',
active: true
},
select: {
id: true,
name: true,
email: true,
role: true,
createdAt: true
}
});
return res.status(201).json({
message: 'User registered successfully',
user
});
} catch (error) {
console.error('Registration error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
} else if (req.method === 'POST' && req.url === '/api/login') {
try {
// Validate input
const validationResult = LoginSchema.safeParse(req.body);
if (!validationResult.success) {
return res.status(400).json({
error: 'Invalid input data'
});
}
const { email, password } = validationResult.data;
// Find user
const user = await prisma.user.findUnique({
where: { email }
});
if (!user || !user.active) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const passwordValid = await compare(password, user.password);
if (!passwordValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate token
const token = sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
return res.status(200).json({
token,
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role
}
});
} catch (error) {
console.error('Login error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
} else if (req.method === 'GET' && req.url === '/api/users') {
try {
// Authenticate and authorize
await authenticate(req, res, async () => {
await authorize(['admin'])(req, res, async () => {
// Pagination parameters
const page = parseInt(req.query.page) || 1;
const limit = Math.min(parseInt(req.query.limit) || 10, 100);
const offset = (page - 1) * limit;
// Fetch users with pagination
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
role: true,
active: true,
createdAt: true
},
skip: offset,
take: limit,
orderBy: { createdAt: 'desc' }
});
// Get total count
const total = await prisma.user.count();
return res.status(200).json({
users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
});
});
} catch (error) {
console.error('Users fetch error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
} else {
return res.status(405).json({ error: 'Method not allowed' });
}
}
This implementation:
- Uses Prisma ORM for database operations
- Implements proper authentication and authorization
- Uses bcrypt for secure password hashing
- Validates input with Zod
- Implements rate limiting
- Provides pagination for data fetching
Base44 Secure Implementation
For Base44’s applications:
// Secure Base44 API implementation
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const { body, param, validationResult } = require('express-validator');
const rateLimit = require('express-rate-limit');
const mongoose = require('mongoose');
const User = require('../models/User');
// Rate limiting middleware
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please try again later' }
});
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10, // Limit each IP to 10 requests per windowMs
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many login attempts, please try again later' }
});
// Apply rate limiting
router.use(apiLimiter);
router.use('/login', authLimiter);
router.use('/register', authLimiter);
// Authentication middleware
function authenticate(req, res, next) {
const token = req.header('Authorization')?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
// Authorization middleware
function authorize(roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Register new user
router.post(
'/register',
[
body('username')
.trim()
.isLength({ min: 3, max: 30 })
.withMessage('Username must be between 3 and 30 characters'),
body('email')
.isEmail()
.withMessage('Must be a valid email address')
.normalizeEmail(),
body('password')
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters long')
.matches(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*])/)
.withMessage('Password must include uppercase, lowercase, number, and special character')
],
async (req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { username, email, password } = req.body;
try {
// Check if user already exists
const existingUser = await User.findOne({
$or: [{ email }, { username }]
});
if (existingUser) {
return res.status(409).json({
error: 'User already exists with that email or username'
});
}
// Hash password
const salt = await bcrypt.genSalt(12);
const hashedPassword = await bcrypt.hash(password, salt);
// Create new user
const user = new User({
username,
email,
password: hashedPassword,
role: 'user'
});
await user.save();
res.status(201).json({
message: 'User registered successfully',
userId: user._id
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
);
// Login
router.post(
'/login',
[
body('email')
.isEmail()
.withMessage('Must be a valid email address')
.normalizeEmail(),
body('password')
.notEmpty()
.withMessage('Password is required')
],
async (req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password } = req.body;
try {
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Check password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate token
const token = jwt.sign(
{
userId: user._id,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({
token,
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
);
// Get user profile
router.get(
'/users/profile',
authenticate,
async (req, res) => {
try {
const user = await User.findById(req.user.userId)
.select('-password');
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
console.error('Profile fetch error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
);
// Get all users (admin only)
router.get(
'/users',
authenticate,
authorize(['admin']),
async (req, res) => {
try {
// Pagination
const page = parseInt(req.query.page) || 1;
const limit = Math.min(parseInt(req.query.limit) || 10, 100);
const skip = (page - 1) * limit;
// Get users with pagination
const users = await User.find()
.select('-password')
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 });
// Get total count
const total = await User.countDocuments();
res.json({
users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('Users fetch error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
);
// Update user
router.put(
'/users/:id',
authenticate,
[
param('id')
.custom(value => mongoose.Types.ObjectId.isValid(value))
.withMessage('Invalid user ID'),
body('username')
.optional()
.trim()
.isLength({ min: 3, max: 30 })
.withMessage('Username must be between 3 and 30 characters'),
body('email')
.optional()
.isEmail()
.withMessage('Must be a valid email address')
.normalizeEmail(),
body('password')
.optional()
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters long')
.matches(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*])/)
.withMessage('Password must include uppercase, lowercase, number, and special character')
],
async (req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const userId = req.params.id;
// Check if user is updating their own profile or is an admin
if (req.user.userId !== userId && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Insufficient permissions' });
}
try {
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Prepare update data
const updateData = {};
if (req.body.username) updateData.username = req.body.username;
if (req.body.email) updateData.email = req.body.email;
if (req.body.password) {
const salt = await bcrypt.genSalt(12);
updateData.password = await bcrypt.hash(req.body.password, salt);
}
// Only admins can update role
if (req.body.role && req.user.role === 'admin') {
updateData.role = req.body.role;
}
// Update user
const updatedUser = await User.findByIdAndUpdate(
userId,
updateData,
{ new: true }
).select('-password');
res.json({
message: 'User updated successfully',
user: updatedUser
});
} catch (error) {
console.error('Update error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
);
// Delete user
router.delete(
'/users/:id',
authenticate,
[
param('id')
.custom(value => mongoose.Types.ObjectId.isValid(value))
.withMessage('Invalid user ID')
],
async (req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const userId = req.params.id;
// Check if user is deleting their own account or is an admin
if (req.user.userId !== userId && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Insufficient permissions' });
}
try {
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
await User.findByIdAndDelete(userId);
res.json({ message: 'User deleted successfully' });
} catch (error) {
console.error('Delete error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
);
module.exports = router;
This implementation:
- Uses bcrypt for secure password hashing
- Implements JWT-based authentication
- Uses express-validator for input validation
- Implements role-based authorization
- Provides pagination for data fetching
- Implements rate limiting
- Handles errors gracefully
Replit Secure Implementation
For Replit’s Python Flask applications:
# Secure Replit API implementation
from flask import Flask, request, jsonify, g
from flask_cors import CORS
from werkzeug.security import generate_password_hash, check_password_hash
import sqlite3
import jwt
import datetime
import os
import re
from functools import wraps
import time
from threading import Lock
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('JWT_SECRET_KEY', 'default-dev-key-change-in-production')
# Configure CORS
CORS(app, resources={
r"/api/*": {
"origins": [
"https://yourapplication.com",
"https://admin.yourapplication.com"
],
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization"]
}
})
# Database setup
DATABASE = 'database.db'
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
# Initialize database
def init_db():
with app.app_context():
db = get_db()
with app.open_resource('schema.sql', mode='r') as f:
db.cursor().executescript(f.read())
db.commit()
# Rate limiting
class RateLimiter:
def __init__(self):
self.ip_cache = {}
self.lock = Lock()
def is_rate_limited(self, ip, limit, window):
with self.lock:
now = time.time()
if ip not in self.ip_cache:
self.ip_cache[ip] = {'count': 1, 'window_start': now}
return False
cache_data = self.ip_cache[ip]
# Reset if window has passed
if now - cache_data['window_start'] > window:
cache_data['count'] = 1
cache_data['window_start'] = now
return False
# Check if over limit
if cache_data['count'] >= limit:
return True
# Increment count
cache_data['count'] += 1
return False
rate_limiter = RateLimiter()
# Authentication decorator
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = None
# Check for rate limiting
ip = request.remote_addr
if rate_limiter.is_rate_limited(ip, 100, 60 * 15): # 100 requests per 15 minutes
return jsonify({'error': 'Rate limit exceeded'}), 429
# Get token from header
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
token = auth_header.split(' ')[1]
if not token:
return jsonify({'error': 'Authentication required'}), 401
try:
# Decode token
data = jwt.decode(
token,
app.config['SECRET_KEY'],
algorithms=['HS256']
)
# Get user from database
db = get_db()
user = db.execute(
'SELECT id, username, email, role, active FROM users WHERE id = ?',
(data['user_id'],)
).fetchone()
if not user or not user['active']:
return jsonify({'error': 'Invalid or inactive user'}), 401
# Store user in request context
g.user = {
'id': user['id'],
'username': user['username'],
'email': user['email'],
'role': user['role']
}
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token has expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token'}), 401
return f(*args, **kwargs)
return decorated
# Role-based authorization decorator
def role_required(roles):
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
if not hasattr(g, 'user'):
return jsonify({'error': 'Authentication required'}), 401
if g.user['role'] not in roles:
return jsonify({'error': 'Insufficient permissions'}), 403
return f(*args, **kwargs)
return decorated
return decorator
# Input validation functions
def validate_username(username):
if not username or not isinstance(username, str):
return False
if len(username) < 3 or len(username) > 30:
return False
if not re.match(r'^[a-zA-Z0-9_]+$', username):
return False
return True
def validate_email(email):
if not email or not isinstance(email, str):
return False
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email):
return False
return True
def validate_password(password):
if not password or not isinstance(password, str):
return False
if len(password) < 8:
return False
if not re.search(r'[A-Z]', password):
return False
if not re.search(r'[a-z]', password):
return False
if not re.search(r'[0-9]', password):
return False
if not re.search(r'[!@#$%^&*]', password):
return False
return True
# Register endpoint
@app.route('/api/register', methods=['POST'])
def register():
# Check for rate limiting
ip = request.remote_addr
if rate_limiter.is_rate_limited(ip, 10, 60 * 60): # 10 requests per hour
return jsonify({'error': 'Rate limit exceeded'}), 429
data = request.get_json()
# Validate input
if not data:
return jsonify({'error': 'No input data provided'}), 400
username = data.get('username')
email = data.get('email')
password = data.get('password')
if not validate_username(username):
return jsonify({'error': 'Invalid username format'}), 400
if not validate_email(email):
return jsonify({'error': 'Invalid email format'}), 400
if not validate_password(password):
return jsonify({'error': 'Password does not meet requirements'}), 400
db = get_db()
# Check if user already exists
existing_user = db.execute(
'SELECT id FROM users WHERE email = ? OR username = ?',
(email, username)
).fetchone()
if existing_user:
return jsonify({'error': 'User already exists with that email or username'}), 409
# Hash password
hashed_password = generate_password_hash(password, method='pbkdf2:sha256', salt_length=16)
# Create user
db.execute(
'INSERT INTO users (username, email, password, role, active) VALUES (?, ?, ?, ?, ?)',
(username, email, hashed_password, 'user', 1)
)
db.commit()
return jsonify({'message': 'User registered successfully'}), 201
# Login endpoint
@app.route('/api/login', methods=['POST'])
def login():
# Check for rate limiting
ip = request.remote_addr
if rate_limiter.is_rate_limited(ip, 10, 60 * 60): # 10 requests per hour
return jsonify({'error': 'Rate limit exceeded'}), 429
data = request.get_json()
if not data:
return jsonify({'error': 'No input data provided'}), 400
email = data.get('email')
password = data.get('password')
if not email or not password:
return jsonify({'error': 'Email and password are required'}), 400
db = get_db()
# Find user
user = db.execute(
'SELECT id, username, email, password, role, active FROM users WHERE email = ?',
(email,)
).fetchone()
if not user or not user['active']:
return jsonify({'error': 'Invalid credentials'}), 401
# Check password
if not check_password_hash(user['password'], password):
return jsonify({'error': 'Invalid credentials'}), 401
# Generate token
token = jwt.encode(
{
'user_id': user['id'],
'role': user['role'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
},
app.config['SECRET_KEY'],
algorithm='HS256'
)
return jsonify({
'token': token,
'user': {
'id': user['id'],
'username': user['username'],
'email': user['email'],
'role': user['role']
}
}), 200
# Get user profile
@app.route('/api/users/profile', methods=['GET'])
@token_required
def get_profile():
return jsonify(g.user), 200
# Get all users (admin only)
@app.route('/api/users', methods=['GET'])
@token_required
@role_required(['admin'])
def get_users():
# Pagination
page = request.args.get('page', 1, type=int)
limit = min(request.args.get('limit', 10, type=int), 100)
offset = (page - 1) * limit
db = get_db()
# Get users with pagination
users = db.execute(
'SELECT id, username, email, role, active, created_at FROM users LIMIT ? OFFSET ?',
(limit, offset)
).fetchall()
# Get total count
total = db.execute('SELECT COUNT(*) as count FROM users').fetchone()['count']
return jsonify({
'users': [dict(user) for user in users],
'pagination': {
'page': page,
'limit': limit,
'total': total,
'pages': (total + limit - 1) // limit
}
}), 200
# Update user
@app.route('/api/users/<int:user_id>', methods=['PUT'])
@token_required
def update_user(user_id):
# Check if user is updating their own profile or is an admin
if g.user['id'] != user_id and g.user['role'] != 'admin':
return jsonify({'error': 'Insufficient permissions'}), 403
data = request.get_json()
if not data:
return jsonify({'error': 'No input data provided'}), 400
db = get_db()
# Check if user exists
user = db.execute(
'SELECT id FROM users WHERE id = ?',
(user_id,)
).fetchone()
if not user:
return jsonify({'error': 'User not found'}), 404
# Prepare update data
updates = []
params = []
if 'username' in data:
if not validate_username(data['username']):
return jsonify({'error': 'Invalid username format'}), 400
updates.append('username = ?')
params.append(data['username'])
if 'email' in data:
if not validate_email(data['email']):
return jsonify({'error': 'Invalid email format'}), 400
updates.append('email = ?')
params.append(data['email'])
if 'password' in data:
if not validate_password(data['password']):
return jsonify({'error': 'Password does not meet requirements'}), 400
hashed_password = generate_password_hash(data['password'], method='pbkdf2:sha256', salt_length=16)
updates.append('password = ?')
params.append(hashed_password)
# Only admins can update role
if 'role' in data and g.user['role'] == 'admin':
if data['role'] not in ['user', 'admin']:
return jsonify({'error': 'Invalid role'}), 400
updates.append('role = ?')
params.append(data['role'])
if not updates:
return jsonify({'error': 'No valid fields to update'}), 400
# Add user_id to params
params.append(user_id)
# Update user
db.execute(
f"UPDATE users SET {', '.join(updates)} WHERE id = ?",
params
)
db.commit()
# Get updated user
updated_user = db.execute(
'SELECT id, username, email, role, active, created_at FROM users WHERE id = ?',
(user_id,)
).fetchone()
return jsonify({
'message': 'User updated successfully',
'user': dict(updated_user)
}), 200
# Delete user
@app.route('/api/users/<int:user_id>', methods=['DELETE'])
@token_required
def delete_user(user_id):
# Check if user is deleting their own account or is an admin
if g.user['id'] != user_id and g.user['role'] != 'admin':
return jsonify({'error': 'Insufficient permissions'}), 403
db = get_db()
# Check if user exists
user = db.execute(
'SELECT id FROM users WHERE id = ?',
(user_id,)
).fetchone()
if not user:
return jsonify({'error': 'User not found'}), 404
# Delete user
db.execute('DELETE FROM users WHERE id = ?', (user_id,))
db.commit()
return jsonify({'message': 'User deleted successfully'}), 200
if __name__ == '__main__':
# In production, use a proper WSGI server and set debug=False
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)), debug=False)
This implementation:
- Uses werkzeug for secure password hashing
- Implements JWT-based authentication
- Uses custom input validation
- Implements role-based authorization
- Provides pagination for data fetching
- Implements rate limiting
- Configures CORS properly
- Handles errors gracefully
Best Practices for API Security
To ensure your vibe-coded APIs remain secure, follow these best practices:
1. Use Specific Prompts for Security
When using AI tools to generate API code, include security requirements in your prompts:
Instead of:
Create an API endpoint to get user data
Use:
Create a secure API endpoint to get user data with proper authentication, authorization, input validation, rate limiting, and error handling. Ensure sensitive data is protected and only authorized users can access it.
2. Implement Defense in Depth
Don’t rely on a single security measure:
- Combine authentication, authorization, input validation, and rate limiting
- Use HTTPS for all API traffic
- Implement proper CORS configuration
- Use security headers (Content-Security-Policy, X-Content-Type-Options, etc.)
- Apply the principle of least privilege
3. Secure Authentication and Authorization
Implement robust authentication and authorization:
- Use industry-standard authentication mechanisms (JWT, OAuth 2.0)
- Implement role-based access control
- Enforce the principle of least privilege
- Use secure password storage with proper hashing
- Implement token expiration and refresh mechanisms
- Consider multi-factor authentication for sensitive operations
4. Validate and Sanitize All Input
Never trust client input:
- Validate all input data (type, format, range, etc.)
- Use schema validation libraries (Zod, Joi, express-validator)
- Implement whitelist validation instead of blacklist
- Sanitize data before using it in database queries or responses
- Validate query parameters, URL parameters, headers, and request body
5. Implement Rate Limiting
Protect against abuse:
- Limit request frequency based on IP, user, or API key
- Apply stricter limits to authentication endpoints
- Use token bucket or sliding window algorithms
- Provide clear feedback when limits are exceeded
- Consider dynamic rate limiting based on user roles
6. Use Secure Error Handling
Prevent information leakage:
- Return generic error messages to clients
- Log detailed errors for internal debugging
- Use appropriate HTTP status codes
- Don’t expose stack traces, database errors, or system information
- Implement global error handling middleware
7. Secure Data Transmission
Protect data in transit:
- Enforce HTTPS for all API traffic
- Implement HTTP Strict Transport Security (HSTS)
- Use secure cookies with HttpOnly, Secure, and SameSite flags
- Consider field-level encryption for highly sensitive data
- Implement proper CORS configuration
8. Regular Security Testing
Continuously test for API vulnerabilities:
- Implement automated security testing in CI/CD pipeline
- Use tools like OWASP ZAP, Burp Suite, or Postman for API testing
- Perform regular security audits
- Consider penetration testing for critical APIs
- Monitor for unusual patterns or potential attacks
Conclusion
API security is a critical aspect of application development, especially in the context of vibe coding where AI tools might prioritize functionality over security. By understanding common API security vulnerabilities in AI-generated code and implementing secure alternatives, you can protect your applications from unauthorized access, data breaches, and service disruptions.
The key principles for securing APIs in vibe-coded applications include:
- Implementing proper authentication and authorization to control access to your API
- Validating and sanitizing all input to prevent injection attacks and data corruption
- Implementing rate limiting to protect against abuse and denial of service
- Using secure error handling to prevent information leakage
- Enforcing HTTPS to protect data in transit
- Configuring CORS properly to control cross-origin access
- Following the principle of least privilege to minimize potential damage from compromised accounts
- Regularly testing for security vulnerabilities to identify and address issues early
Each full-stack app builder—Lovable.dev, Bolt.new, Tempo Labs, Base44, and Replit—has its own API implementation patterns and potential vulnerabilities. By applying platform-specific security enhancements and following best practices, you can ensure that your vibe-coded applications maintain robust protection against API-related threats while still benefiting from the rapid development that AI tools enable.
Remember that a single API vulnerability can compromise your entire application. Investing time in securing your APIs will protect your users’ data and your reputation from the devastating consequences of a successful attack.
Additional Resources
- OWASP API Security Top 10
- JWT Best Practices
- REST Security Cheat Sheet
- API Security Best Practices
- Express.js Security Best Practices
- Flask Security Considerations
References
- Salt Security. (2023). “State of API Security Report.” Retrieved from https://salt.security/api-security-trends
- OWASP. (2023). “API Security Top 10.” Retrieved from https://owasp.org/API-Security/editions/2023/en/0x00-introduction/
- Ping Identity. (2022). “API Security: The Complete Guide.” Retrieved from https://www.pingidentity.com/en/resources/blog/post/complete-guide-api-security.html
- Postman. (2023). “2023 State of the API Report.” Retrieved from https://www.postman.com/state-of-api/
- Gartner. (2022). “API Security: Protecting Your Application Programming Interfaces.” Retrieved from https://www.gartner.com/en/documents/4016095