A scalable, cloud-native microservice for managing data fragments with secure REST API and automated CI/CD deployment.
The Fragments Microservice is a scalable backend service built on AWS to manage small pieces of data (“fragments”) such as text, JSON, CSV, YAML, Markdown, and images. It provides a secure REST API supporting full CRUD operations with JWT-based authentication, ensuring that data remains private and isolated per user. Binary data is stored in Amazon S3, while fragment metadata is managed in DynamoDB for fast retrieval and scalability. The service also supports on-the-fly format conversions (e.g., Markdown ➝ HTML) to reduce storage overhead. Deployment is automated with a CI/CD pipeline using GitHub Actions, Docker, ECS, and ECR, with testing handled via Jest and Hurl. This microservice is designed to power the Fragments Client App and demonstrate a modern, production-ready architecture for data management in distributed systems.
- Multi-Format Support: Text, JSON, CSV, YAML, Markdown, and image formats
- Format Conversion: On-the-fly conversions (e.g., Markdown → HTML)
- Secure Authentication: JWT-based auth with AWS Cognito
- Cloud Storage: Binary data in S3, metadata in DynamoDB
- RESTful API: Full CRUD operations with intuitive endpoints
- Image Processing: Built-in image manipulation capabilities
- Automated Testing: Unit and integration test suites
- CI/CD Pipeline: Automated deployment with GitHub Actions
- Containerized: Docker support for consistent environments
- Monitoring: Structured logging with Pino
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Client Apps │────│ Fragments API │────│ AWS Services │
│ │ │ │ │ │
│ • Web App │ │ • Express.js │ │ • S3 (Storage) │
│ • Mobile App │ │ • JWT Auth │ │ • DynamoDB │
│ • CLI Tools │ │ • Format Conv. │ │ • Cognito │
└─────────────────┘ └─────────────────┘ └─────────────────┘
- Runtime: Node.js with Express.js v5.1.0 framework
- Authentication: AWS Cognito with JWT verification, Passport.js
- Storage: Amazon S3 for binary data, DynamoDB for metadata
- Content Processing: Sharp (images), Markdown-it (markdown), js-yaml (YAML)
- Testing: Jest (unit tests), Hurl (integration tests), Supertest (HTTP testing)
- Deployment: Docker, AWS ECS, ECR
- CI/CD: GitHub Actions
- Logging: Pino structured logging
Before running the Fragments Microservice, ensure you have:
- Node.js (v16.0.0 or higher)
- npm (v8.0.0 or higher)
- AWS Account with appropriate permissions
- Docker (optional, for containerized deployment)
- curl and jq (for testing API endpoints)
- AWS Cognito User Pool
- Amazon S3 Bucket
- DynamoDB Table
- IAM Roles with appropriate permissions
-
Clone the repository
git clone https://github.com/kolossi101/fragments cd fragments -
Install dependencies
npm install
-
Set up environment configuration
cp .env.example .env # Edit .env with your configuration values
Create a .env file in the project root with the following variables:
# Server Configuration
PORT=8080
NODE_ENV=development
LOG_LEVEL=debug
# AWS Configuration
AWS_REGION=us-east-1
AWS_COGNITO_POOL_ID=your_user_pool_id
AWS_COGNITO_CLIENT_ID=your_client_app_id
# Storage Configuration
AWS_S3_BUCKET_NAME=your-fragments-bucket
AWS_DYNAMODB_TABLE_NAME=fragments
# Authentication (for development/testing)
HTPASSWD_FILE=tests/.htpasswd
# Optional: API Configuration
API_URL=http://localhost:8080| Variable | Description | Default | Required |
|---|---|---|---|
PORT |
Server port | 8080 |
No |
NODE_ENV |
Environment mode | development |
No |
LOG_LEVEL |
Logging level | info |
No |
AWS_COGNITO_POOL_ID |
Cognito User Pool ID | - | Yes |
AWS_COGNITO_CLIENT_ID |
Cognito Client App ID | - | Yes |
HTPASSWD_FILE |
Basic auth file path | - | No |
Start the server with hot reload:
npm run devStart the server in production:
npm startStart with Node.js inspector enabled:
npm run debugThe server will be available at http://localhost:8080
http://localhost:8080/v1
The API supports two authentication methods:
-
JWT Bearer Token (Production)
Authorization: Bearer <jwt_token>
-
Basic Authentication (Development)
Authorization: Basic <base64_credentials>
| Method | Endpoint | Description |
|---|---|---|
GET |
/fragments |
List user's fragments |
GET |
/fragments/:id |
Get fragment data |
GET |
/fragments/:id/info |
Get fragment metadata |
POST |
/fragments |
Create new fragment |
PUT |
/fragments/:id |
Update existing fragment |
DELETE |
/fragments/:id |
Delete fragment |
Warning: First
POSTa fragment and paste into<FRAGMENT-ID>to work with GET method
curl -i \
-X POST \
-u user1@email.com:password1 \
-H "Content-Type: text/plain" \
-d "This is a fragment" \
https://fragments-api.com/v1/fragmentsResponse:
{
"status": "ok",
"fragment": {
"id": "30a84843-0cd4-4975-95ba-b96112aea189",
"created": "2021-11-08T01:04:46.071Z",
"updated": "2021-11-08T01:04:46.073Z",
"ownerId": "11d4c22e42c8f61feaba154683dea407b101cfd90987dda9e342843263ca420a",
"type": "text/plain",
"size": 18
}
}curl -i \
-X POST \
-u user1@email.com:password1 \
-H "Content-Type: text/markdown" \
-d "# My Fragment\n\nThis is **bold** text." \
https://fragments-api.com/v1/fragmentscurl -i \
-X POST \
-u user1@email.com:password1 \
-H "Content-Type: application/json" \
-d '{"message": "Hello", "type": "greeting"}' \
https://fragments-api.com/v1/fragmentscurl -i -u user1@email.com:password1 https://fragments-api.com/v1/fragments/{fragment-id}curl -i \
-u user1@email.com:password1 \
https://fragments-api.com/v1/fragments/{fragment-id}/info# Basic list
curl -s -u user@example.com:password10 \
"http://localhost:8080/v1/fragments" | jq
# Expanded list with metadata
curl -s -u user@example.com:password10 \
"http://localhost:8080/v1/fragments?expand=1" | jqcurl -i -X PUT \
-u user@example.com:password10 \
-H "Content-Type: text/plain" \
-d "Updated content here" \
"http://localhost:8080/v1/fragments/{fragment-id}"curl -i -X DELETE \
-u user@example.com:password10 \
"http://localhost:8080/v1/fragments/{fragment-id}"| Type | MIME Type | Extensions | Conversions |
|---|---|---|---|
| Text | text/plain |
.txt |
- |
| Markdown | text/markdown |
.md |
→ HTML |
| HTML | text/html |
.html |
- |
| JSON | application/json |
.json |
- |
| YAML | text/yaml |
.yaml, .yml |
- |
| CSV | text/csv |
.csv |
- |
| Images | image/png, image/jpeg, image/webp, image/gif |
Various | Format conversion, resize |
Each fragment consists of:
- Metadata: Information about the fragment (id, type, size, dates)
- Data: The actual binary content
Fragment Metadata:
{
"id": "30a84843-0cd4-4975-95ba-b96112aea189",
"ownerId": "11d4c22e42c8f61feaba154683dea407b101cfd90987dda9e342843263ca420a",
"created": "2021-11-02T15:09:50.403Z",
"updated": "2021-11-02T15:09:50.403Z",
"type": "text/plain",
"size": 256
}Text Fragment:
curl -i -u user@example.com:password10 \
-H "Content-Type: text/plain" \
-d "This is a text fragment" \
-X POST http://localhost:8080/v1/fragments | jqMarkdown Fragment:
curl -i -u user@example.com:password10 \
-H "Content-Type: text/markdown" \
-d "# My Fragment\n\nThis is **bold** text." \
-X POST http://localhost:8080/v1/fragments | jqJSON Fragment:
curl -i -u user@example.com:password10 \
-H "Content-Type: application/json" \
-d '{"coffee": "macchiato", "size": "large"}' \
-X POST http://localhost:8080/v1/fragments | jqResponse (HTTP 201):
{
"status": "ok",
"fragment": {
"id": "30a84843-0cd4-4975-95ba-b96112aea189",
"ownerId": "11d4c22e42c8f61feaba154683dea407b101cfd90987dda9e342843263ca420a",
"created": "2021-11-02T15:09:50.403Z",
"updated": "2021-11-02T15:09:50.403Z",
"type": "text/plain",
"size": 256
}
}Get Fragment Data:
curl -i -u user@example.com:password10 \
"http://localhost:8080/v1/fragments/{fragment-id}"
# Returns raw binary data with appropriate Content-Type headerGet Fragment Metadata:
curl -i -u user@example.com:password10 \
"http://localhost:8080/v1/fragments/{fragment-id}/info" | jqBasic List (IDs only):
curl -i -u user@example.com:password10 \
"http://localhost:8080/v1/fragments" | jq
# Response:
{
"status": "ok",
"fragments": [
"4dcc65b6-9d57-453a-bd3a-63c107a51698",
"30a84843-0cd4-4975-95ba-b96112aea189"
]
}Expanded List (Full Metadata):
curl -i -u user@example.com:password10 \
"http://localhost:8080/v1/fragments?expand=1" | jq
# Returns array of complete fragment metadata objectscurl -i -X PUT \
-u user@example.com:password10 \
-H "Content-Type: text/plain" \
-d "This is updated data" \
"http://localhost:8080/v1/fragments/{fragment-id}"
# Note: Content-Type must match the original fragment typecurl -i -X DELETE \
-u user@example.com:password10 \
"http://localhost:8080/v1/fragments/{fragment-id}"
# Response (HTTP 200):
{ "status": "ok" }| Type | MIME Type | Extensions | Description |
|---|---|---|---|
| Text | text/plain |
.txt |
Plain text content |
| Markdown | text/markdown |
.md |
Markdown formatted text |
| HTML | text/html |
.html |
HTML markup |
| CSV | text/csv |
.csv |
Comma-separated values |
| JSON | application/json |
.json |
JSON data |
| YAML | application/yaml |
.yaml, .yml |
YAML data |
| PNG | image/png |
.png |
PNG images |
| JPEG | image/jpeg |
.jpg |
JPEG images |
| WebP | image/webp |
.webp |
WebP images |
| AVIF | image/avif |
.avif |
AVIF images |
| GIF | image/gif |
.gif |
GIF images |
Fragments can be converted to different formats by appending file extensions to the fragment ID:
| Original Type | Available Conversions |
|---|---|
text/plain |
.txt |
text/markdown |
.md, .html, .txt |
text/html |
.html, .txt |
text/csv |
.csv, .txt, .json |
application/json |
.json, .yaml, .yml, .txt |
application/yaml |
.yaml, .txt |
| All Images | .png, .jpg, .webp, .gif, .avif |
# Get Markdown fragment as HTML
curl -u user@example.com:password10 \
"http://localhost:8080/v1/fragments/markdown-id.html"
# Get JSON as YAML
curl -u user@example.com:password10 \
"http://localhost:8080/v1/fragments/json-id.yaml"
# Convert image formats
curl -u user@example.com:password10 \
"http://localhost:8080/v1/fragments/image-id.png"| Code | Description |
|---|---|
400 |
Bad Request - Invalid request format or data |
401 |
Unauthorized - Authentication required |
404 |
Not Found - Fragment doesn't exist or doesn't belong to user |
415 |
Unsupported Media Type - Invalid content type or conversion |
500 |
Internal Server Error - Server-side error |
- All fragments are private and isolated per authenticated user
- Users can only access their own fragments
- Fragment ownership is determined by the
ownerId(SHA256 hash of email) - No public access to any fragment data
The project includes comprehensive testing suites:
# Run all unit tests
npm test
# Run tests in watch mode
npm run test:watch
# Generate coverage report
npm run coverage
# Run integration tests
npm run test:integration
# Run linting
npm run linttests/
├── unit/ # Jest unit tests
├── integration/ # Hurl integration tests
├── images/ # Test image data
└── .htpasswd # Basic auth credentials for testing
Unit tests use Jest and should be placed in the tests/unit/ directory:
// Example test
describe('Fragment', () => {
test('should create valid fragment', () => {
const fragment = new Fragment({
ownerId: 'user123',
type: 'text/plain',
});
expect(fragment.type).toBe('text/plain');
});
});-
Build Docker image
docker build -t fragments-microservice . -
Run container
docker run -p 8080:8080 --env-file .env fragments-microservice
The service includes automated deployment via GitHub Actions:
-
Set up GitHub Secrets:
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_REGION- Other environment variables
-
Deploy: Push to main branch triggers automatic deployment
- Create S3 bucket for fragment storage
- Set up DynamoDB table for metadata
- Configure Cognito User Pool for authentication
- Deploy to ECS using provided task definition
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Follow ESLint configuration
- Write tests for new features
- Update documentation as needed
- Ensure all tests pass before submitting PR
Port already in use
# Find process using port 8080
lsof -i :8080
# Kill the process
kill -9 <PID>AWS credentials not found
- Ensure AWS CLI is configured:
aws configure - Or set environment variables in
.env
Fragment not found
- Check fragment ID is correct
- Verify user authentication
- Ensure fragment belongs to authenticated user
Docker build fails
- Check Docker daemon is running
- Verify all required files are present
- Review Dockerfile for syntax errors
Enable debug logging:
LOG_LEVEL=debug npm run devCheck service health:
curl http://localhost:8080/View application logs:
# In development
npm run dev
# In Docker
docker logs <container-id>