6 min read
Learn how to build scalable, secure APIs from design to production. Explore real implementation patterns, security strategies, and best practices with practical examples from open-source projects.
Once you've finished designing your API contract and defining your endpoints, you can transform your blueprint into a working solution.
This phase takes you from specification to a production-ready API that handles real traffic, scales correctly, and stays secure.
Let's explore how to build reliable APIs in production, using lessons from real projects like the AI Prompt Enhancer, an open-source API that demonstrates many of the concepts we'll cover.
Your project structure sets the foundation for everything that follows. A well-organized codebase makes debugging easier, onboarding faster, and maintenance less painful.
Here's a structure that separates concerns cleanly:
your-api/
├── src/
│ ├── config/ # Database connections, environment vars
│ ├── controllers/ # Handle HTTP requests and responses
│ ├── middleware/ # Authentication, logging, rate limiting
│ ├── routes/ # URL routing and endpoint definitions
│ ├── services/ # Business logic and external API calls
│ └── utils/ # Helper functions and shared utilities
├── tests/ # Unit, integration, and security tests
├── docs/ # API documentation and guides
└── tools/ # Scripts for deployment and development
This structure keeps your controllers thin by moving business logic into services. When you need to change how you process data, you modify the service layer without touching your HTTP handling code.
For example, in the Prompt Enhancer API, the controller validates the request and calls a service to enhance the prompt. They only need to update the service layer if they want to switch from OpenAI to a different AI provider.
Most APIs need authentication, but implementing it securely takes planning. JWT tokens offer a solid approach that scales well without requiring server-side session storage.
const jwt = require('jsonwebtoken');
function generateToken(payload) {
return jwt.sign(
{ ...payload, iat: Math.floor(Date.now() / 1000) },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
}
function verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
return null;
}
}
The key details matter here.
Set reasonable expiration times (24 hours works for most use cases), include issued-at timestamps, and always use environment variables for secrets.
Never hardcode JWT secrets in your codebase.
Rate limiting prevents your API from being overwhelmed by too many requests. Without it, a single misbehaving client can crash your entire service.
Implement different limits for different scenarios:
const rateLimits = {
general: { maxRequests: 100, window: '1m' }, // Regular API calls
auth: { maxRequests: 20, window: '1m' }, // Login attempts
heavy: { maxRequests: 10, window: '1m' } // Expensive operations
};
The Prompt Enhancer API uses this approach.
Authentication endpoints get stricter limits because failed login attempts often indicate attacks. Heavy operations like AI prompt enhancement get lower limits because they consume more resources.
Set up your rate limiting to return helpful headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640995200
These headers help client developers understand their usage and implement proper retry logic.
Good error responses differentiate between an API developers love and one they abandon. Your errors should be consistent, informative, and secure.
Structure your errors like this:
{
"error": {
"code": "validation_error",
"message": "The request data is invalid",
"details": {
"field": "email",
"reason": "Invalid email format"
}
}
}
Include enough information for debugging without exposing sensitive system details. Log the complete error details server-side in production, but return sanitized messages to clients.
Validate every piece of data that enters your API. This prevents injection attacks and catches problems early.
function validatePromptRequest(req, res) {
const { text, format = 'structured' } = req.body;
if (!text || typeof text !== 'string') {
return res.status(400).json({
error: { code: 'missing_text', message: 'Text field is required' }
});
}
if (text.length > 5000) {
return res.status(413).json({
error: { code: 'text_too_long', message: 'Text must be under 5000 characters' }
});
}
return true;
}
Always sanitize inputs to prevent XSS attacks. Use libraries like DOMPurify for HTML content or implement your sanitization for specific data types.
Most APIs need to call external services. Design these integrations to handle failures gracefully and switch providers when needed.
Here's how the Prompt Enhancer handles multiple AI providers:
async function enhancePrompt(params) {
const provider = process.env.AI_PROVIDER || 'openai';
try {
if (provider === 'mistral') {
return await enhanceWithMistral(params);
} else {
return await enhanceWithOpenAI(params);
}
} catch (error) {
// Fall back to a simple enhancement if the AI service fails
return generateFallbackPrompt(params);
}
}
This pattern lets you switch between providers through configuration and provides fallbacks when services are unavailable. Your API stays functional even when external dependencies fail.
Once your API is running, you need visibility into its performance. Request logging, error tracking, and performance monitoring help you catch issues before they impact users.
Treblle provides comprehensive API observability that tracks every request and response automatically.
Instead of building custom logging infrastructure, you can integrate Treblle to monitor your endpoints' response times, error rates, and usage patterns.
const treblle = require('@treblle/express');
app.use(treblle({
apiKey: process.env.TREBLLE_API_KEY,
projectId: process.env.TREBLLE_PROJECT_ID
}));
This gives you real-time insights into API performance without writing custom monitoring code. You can track the slowest endpoints, identify error patterns, and understand how developers use your API.
Never commit secrets to your repository. Use environment variables for all sensitive configuration:
# .env (never commit this file)
NODE_ENV=production
API_KEY=your_generated_api_key
OPENAI_API_KEY=sk-your_openai_key
JWT_SECRET=your_jwt_secret
TREBLLE_API_KEY=your_treblle_key
The Prompt Enhancer project includes tools for secure key management. Its setup script automatically generates development API keys and provides utilities for encrypting keys for backup purposes.
Create different configurations for development, staging, and production. Development should use test keys and relaxed security, while production needs real secrets and strict validation.
Implement response compression to reduce bandwidth usage:
app.use(compression({
filter: (req, res) => {
return /json|text|javascript|css|xml|svg/.test(
res.getHeader('Content-Type')
);
},
level: 6
}));
Add caching for frequently requested data: cache external API responses, database queries, and computed results. Set appropriate TTL values based on how often your data changes.
Properly handle timeouts throughout your application. Set timeouts for database operations, external API calls, and overall request processing. The Prompt Enhancer uses different timeout values for different operations: shorter for quick validation and longer for AI processing.
The patterns and practices covered here will help you create APIs developers trust and enjoy using. Remember that good APIs are built iteratively - start with the basics, test thoroughly, and improve based on real usage data.