A complete guide to creating, deploying, and scaling a RESTful API service
Introduction
Ever wondered what makes a string special? Is it a palindrome? How many unique characters does it have? What's its cryptographic fingerprint?
I recently built a String Analyzer API that answers all these questions and more. In this post, I'll walk you through the entire journey—from the first line of code to a production deployment on AWS EC2 with Nginx as a reverse proxy.
What Does It Do?
The String Analyzer API is a RESTful service that accepts any string and returns comprehensive analysis including:
- Length and word count
- Palindrome detection (case-insensitive)
- Unique character count
- Character frequency mapping
- SHA256 hash for unique identification
- Advanced filtering with multiple criteria
- Natural language queries ("show me single word palindromes")
Here's what happens when you analyze the string "ursula is a devops engineer":
{
"id": "8f3e825d9f5b2c7a4e1f6d9c3b8a5e2f...",
"value": "ursula is a devops engineer",
"properties": {
"length": 28,
"is_palindrome": false,
"unique_characters": 15,
"word_count": 5,
"sha256_hash": "8f3e825d9f5b2c7a4e1f6d9c3b8a5e2f...",
"character_frequency_map": {
"u": 2,
"r": 2,
"s": 3,
"l": 1,
"a": 2,
"i": 2,
"d": 1,
"e": 4,
"v": 1,
"o": 1,
"p": 1,
"n": 2,
"g": 1
}
},
"created_at": "2025-10-20T12:00:00.000Z"
}
The Tech Stack
I chose a modern, lightweight stack that prioritizes performance and simplicity:
- Node.js + Express: Fast, minimal web framework
- Crypto module: Built-in SHA256 hashing
- In-memory storage: Lightning-fast data access (perfect for MVP)
- PM2: Process management for production
- Nginx: Reverse proxy for clean URLs
- AWS EC2: Free tier Ubuntu instance
No databases, no heavy ORMs—just pure JavaScript efficiency.
Architecture Decisions
1. In-Memory Storage
Instead of reaching for MongoDB or PostgreSQL, I implemented a simple Map-based storage system. Why?
- Speed: Microsecond access times
- Simplicity: No schema migrations or connection pools
- Perfect for MVP: Easy to swap later if needed
const stringStore = new Map();
const store = {
set(id, data) {
stringStore.set(id, data);
},
get(id) {
return stringStore.get(id);
},
getAll() {
return Array.from(stringStore.values());
}
};
2. SHA256 as Primary Key
Each string gets a unique identifier based on its SHA256 hash. This means:
- No duplicate strings (hash collision = duplicate detected)
- Content-addressable storage
- Cryptographically secure IDs
- Same string always gets same ID
3. RESTful Design
The API follows REST principles strictly:
-
POST /strings
- Create -
GET /strings/:stringValue
- Read one -
GET /strings
- Read all (with filters) -
DELETE /strings/:stringValue
- Delete -
GET /strings/filter-by-natural-language
- Natural language queries
4. Natural Language Processing
The coolest feature? You can query in plain English:
GET /strings/filter-by-natural-language?query=single word palindromic strings
The API parses your intent and translates it to filters:
{
"parsed_filters": {
"word_count": 1,
"is_palindrome": true
}
}
Implementation Highlights
String Analysis Algorithm
The core analysis function is elegantly simple:
function analyzeString(value) {
// Length
const length = value.length;
// Palindrome check (case-insensitive, ignore spaces)
const cleanString = value.toLowerCase().replace(/\s/g, '');
const is_palindrome = cleanString === cleanString.split('').reverse().join('');
// Unique characters
const unique_characters = new Set(value.toLowerCase()).size;
// Word count
const word_count = value.trim().split(/\s+/).filter(w => w.length > 0).length;
// SHA256 hash
const sha256_hash = crypto.createHash('sha256').update(value).digest('hex');
// Character frequency
const character_frequency_map = {};
for (const char of value.toLowerCase()) {
if (char !== ' ') {
character_frequency_map[char] = (character_frequency_map[char] || 0) + 1;
}
}
return { length, is_palindrome, unique_characters, word_count, sha256_hash, character_frequency_map };
}
Advanced Filtering
Users can combine multiple filters:
GET /strings?is_palindrome=true&min_length=5&word_count=1&contains_character=a
The controller chains filters efficiently:
let results = store.getAll();
if (is_palindrome !== undefined) {
results = results.filter(s => s.properties.is_palindrome === (is_palindrome === 'true'));
}
if (min_length !== undefined) {
results = results.filter(s => s.properties.length >= parseInt(min_length));
}
// ... more filters
Error Handling
Proper HTTP status codes make the API predictable:
-
201 Created
- String successfully analyzed -
400 Bad Request
- Missing or invalid parameters -
404 Not Found
- String doesn't exist -
409 Conflict
- String already exists -
422 Unprocessable Entity
- Wrong data type -
500 Internal Server Error
- Something went wrong
Deployment Journey
Step 1: Local Development
Started with a clean Node.js setup:
mkdir string-analyzer-api
cd string-analyzer-api
npm init -y
npm install express body-parser cors dotenv crypto
npm install --save-dev nodemon
Organized the code into a clean structure:
src/
├── controllers/ # Request handlers
├── routes/ # API endpoints
├── storage/ # Data persistence
├── utils/ # Business logic
└── server.js # Entry point
Step 2: AWS EC2 Setup
Launched a t2.micro Ubuntu instance (free tier):
-
Security Group Configuration:
- Port 22 (SSH) - for admin access
- Port 80 (HTTP) - for public API access
- Port 3000 (optional) - for direct Node.js access
System Preparation:
sudo apt update && sudo apt upgrade -y
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs git nginx
Step 3: Process Management with PM2
PM2 keeps the application running 24/7:
sudo npm install -g pm2
pm2 start src/server.js --name string-analyzer
pm2 startup # Auto-start on reboot
pm2 save # Save current process list
Benefits of PM2:
- Auto-restart on crashes
- Load balancing (can run multiple instances)
- Log management
- Monitoring dashboard
Step 4: Nginx Reverse Proxy
Instead of exposing port 3000, I configured Nginx to forward traffic:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
This gives us:
- Clean URLs (no :3000 suffix)
- SSL termination capability (add Let's Encrypt later)
- Load balancing (scale horizontally if needed)
- Static file serving (future expansion)
Testing It Out
Let's create and query some strings:
Create a string:
curl -X POST http://your-api.com/strings \
-H "Content-Type: application/json" \
-d '{"value":"ursula is a devops engineer"}'
Query with filters:
curl "http://your-api.com/strings?word_count=5&min_length=20"
Natural language search:
curl "http://your-api.com/strings/filter-by-natural-language?query=strings longer than 25 characters"
Retrieve specific string:
curl "http://your-api.com/strings/ursula%20is%20a%20devops%20engineer"
Performance Considerations
In-Memory Trade-offs
Pros:
- Sub-millisecond response times
- No network latency
- Simple implementation
- Perfect for caching layer
Cons:
- Data lost on restart
- Limited by RAM
- Single-server limitation
Solution for Production:
- Add Redis for persistent caching
- Use PostgreSQL for long-term storage
- Implement write-through cache pattern
Scalability Path
Current setup handles ~1000 requests/second on t2.micro. To scale:
- Vertical: Upgrade to larger EC2 instance
- Horizontal: Add load balancer + multiple instances
- Caching: Add CloudFront CDN for GET requests
- Database: Migrate to RDS for persistence
Lessons Learned
1. Start Simple
I could have added a database from day one, but in-memory storage taught me to focus on core features first. Premature optimization is real.
2. REST Principles Matter
Following RESTful conventions made the API intuitive. Developers know exactly what POST /strings
does.
3. Error Messages Save Time
Clear, specific error messages saved hours of debugging. "Missing 'value' field" beats "Bad Request" every time.
4. Natural Language is Hard
Building even a simple NLP parser was challenging. Regex patterns only go so far—production systems need better approaches.
5. DevOps is Part of Development
Setting up PM2 and Nginx taught me that deployment is as important as code quality. An app that crashes in production is a failed app.
Future Enhancements
Here's what's on the roadmap:
- Persistent Storage: Add PostgreSQL with automatic migration
- Authentication: JWT-based API keys
- Rate Limiting: Prevent abuse
- Webhooks: Notify on new strings
- Advanced NLP: Better natural language understanding
- Batch Processing: Analyze multiple strings at once
- String Comparison: Find similar strings
- Export Formats: CSV, JSON, XML downloads
- Analytics Dashboard: Visualize string patterns
Try It Yourself
The entire project is open source and deployment-ready:
- Live API: Check out the working demo
- GitHub Repo: Clone and deploy in minutes
- Full Documentation: Complete API reference included
Whether you're learning Node.js, practicing DevOps, or building a production service, this project covers the full stack from code to cloud.
Conclusion
Building this String Analyzer API was a fantastic journey through modern web development. From Express routing to AWS deployment, every step taught valuable lessons about creating production-ready services.
The best part? It took less than a day from first commit to live deployment. That's the power of choosing the right tools and keeping things simple.
What would you analyze with this API? What features would you add? Drop your thoughts in the comments!
Tech Stack: Node.js · Express · AWS EC2 · Nginx · PM2
Deployment Time: ~2 hours
Lines of Code: ~300
Want to build something similar? Check out the full tutorial and source code on GitHub. Stars and contributions welcome!
Top comments (0)