Simple TypeScript microservice with in-memory storage:
- Create text-only posts
- Like/unlike posts
- Comment on posts
- Node.js + TypeScript
- Express
- Zod for schema validation for entities
- Mocha, Sinon, and Chai for tests
The project follows a layered architecture with clear separation of concerns:
- Web Layer (routes/middleware) - handles HTTP requests/responses
- Application Layer (services) - contains business logic
- Entity Layer (models/validation) - defines entities and validation rules
- Infrastructure Layer (repositories) - abstracts data access
This structure makes the code testable and maintainable. Each layer can be tested independently, and changes in one layer don't affect others.
Repository Pattern
- Data access is abstracted behind interfaces (PostsRepository, CommentsRepository, LikesRepository)
- Currently using in-memory storage, but can easily swap to a database later
Factory Pattern
- Using
createPostsRouter()andcreateServer()functions initialising different repositories
Service Pattern
- Our PostsService contains the entire business logic instead of residing directly in routes
- Routes -> services -> repositories (separation of concerns between layers)
Dependency Injection
- Dependencies are injected through constructors
Middleware Pattern
- Using ErrorHandler and validateUUIDParam() for validating schema via middleware pattern
Chain Of Responsibility Pattern
- Express.JS beautifully implements the COR pattern via the
app.use()&next()call inside the routes. - Multiple handlers/routers can be 'chained' together to achieve modular functionality.
Type Safety
- Using TypeScript in strict mode
- Zod schemas for runtime validation
- Using UUIDv4 from node's
cryptopackage (minimal collisions between generated IDs)
Error Handling
- Declared custom error classes (NotFoundError, ValidationError, ConflictError) for better error handling
- Centralized error handler maps errors to HTTP status codes
The project has comprehensive test coverage (34 tests in total):
Unit Tests (posts.service.utc.test.ts)
- Test pure business logic in isolation
Integration Tests (posts.api.itc.test.ts)
- Test API behavior end-to-end
In-Memory Storage
- Chose in-memory data atructures for simplicity and fast development
- Repository pattern makes it easy to swap to a database later
- Good for the current purpose of Assignment/MVP/demo
Zod for Validation
- Provides type-safe validation
Factory Functions
- Better for testing (can inject dependencies)
- Avoids singleton patterns
- More flexible approach
Custom Error Classes
- Easy to map to HTTP status codes
- Using Map data structures for O(1) constant time access
- Idempotent operations (safe to retry)
- Install dependencies
npm install- Build and start
npm run build
npm startService listens on https://bp-social-network-app.onrender.com
curl https://bp-social-network-app.onrender.com/healthI've used UptimeRobot to perform health monitoring: https://stats.uptimerobot.com/5KtXbR6uN5
I've uploaded the API collection for both Bruno and Postman API client:
- Bruno:
./API Collection_Bruno.json - Postman:
./API Collection_Postman.json
Just download any of the two and import to start using the below mentioned APIs!
Base Hosted URL: https://bp-social-network-app.onrender.com/api/v1/posts
All postId and userId path parameters must be valid UUIDs. Invalid UUIDs will return a 400 Bad Request error.
Creates a new text post.
Request:
POST /posts
Content-Type: application/jsonRequest Body:
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"description": "This is my first post!"
}Validation Rules:
userId: Required, must be a valid UUIDdescription: Required, 1-1000 characters
Response: 201 Created
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"userId": "550e8400-e29b-41d4-a716-446655440000",
"description": "This is my first post!",
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"likeCount": 0,
"commentCount": 0
}Error Responses:
400 Bad Request: Invalid request payload or validation errors
Example:
curl -X POST https://bp-social-network-app.onrender.com/api/v1/posts \
-H 'Content-Type: application/json' \
-d '{"userId":"550e8400-e29b-41d4-a716-446655440000","description":"Hello world!"}'Retrieves all posts, sorted by creation date (newest first).
Request:
GET /postsResponse: 200 OK
[
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"userId": "550e8400-e29b-41d4-a716-446655440000",
"description": "Newest post",
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"likeCount": 5,
"commentCount": 2
},
{
"id": "223e4567-e89b-12d3-a456-426614174001",
"userId": "650e8400-e29b-41d4-a716-446655440001",
"description": "Older post",
"createdAt": 1703980800000,
"updatedAt": 1703980800000,
"likeCount": 10,
"commentCount": 3
}
]Example:
curl https://bp-social-network-app.onrender.com/api/v1/postsRetrieves a specific post by its ID.
Request:
GET /posts/:postIdPath Parameters:
postId: UUID of the post
Response: 200 OK
{
"id": "7982bfb7-1780-4828-8ed6-278d468e08c6",
"userId": "550e8400-e29b-41d4-a716-446655440000",
"description": "This is my first post!",
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"likeCount": 5,
"commentCount": 2
}Error Responses:
400 Bad Request: Invalid UUID format forpostId404 Not Found: Post not found
Example:
curl https://bp-social-network-app.onrender.com/api/v1/posts/5f5ff573-c092-4360-a629-d03bf2d583e8Deletes a post by its ID. Only the post owner can delete their post.
Request:
DELETE /posts/:postId
Content-Type: application/jsonPath Parameters:
postId: UUID of the post to delete
Request Body:
{
"userId": "550e8400-e29b-41d4-a716-446655440000"
}Validation Rules:
userId: Required, must be a valid UUID. Must match the owner of the post.
Response: 204 No Content
Error Responses:
400 Bad Request: Invalid UUID format forpostId, missing or invaliduserId, or user is not the post owner404 Not Found: Post not found
Example:
curl -X DELETE https://bp-social-network-app.onrender.com/api/v1/posts/5f5ff573-c092-4360-a629-d03bf2d583e8 \
-H 'Content-Type: application/json' \
-d '{"userId":"550e8400-e29b-41d4-a716-446655440000"}'Adds a like to a post. Users can only like a post once. Attempting to like the same post again will return a conflict error.
Request:
POST /posts/:postId/like
Content-Type: application/jsonPath Parameters:
postId: UUID of the post to like
Request Body:
{
"userId": "660e8400-e29b-41d4-a716-446655440002"
}Validation Rules:
userId: Required, must be a valid UUID
Response: 201 Created
{
"postId": "7982bfb7-1780-4828-8ed6-278d468e08c6",
"userId": "660e8400-e29b-41d4-a716-446655440002",
"createdAt": 1704067300000
}Error Responses:
400 Bad Request: Invalid UUID format or validation errors404 Not Found: Post not found409 Conflict: User has already liked this post
Example:
curl -X POST https://bp-social-network-app.onrender.com/api/v1/posts/5f5ff573-c092-4360-a629-d03bf2d583e8/like \
-H 'Content-Type: application/json' \
-d '{"userId":"660e8400-e29b-41d4-a716-446655440002"}'Removes a like from a post. This operation is idempotent - removing a non-existent like will succeed silently.
Request:
DELETE /posts/:postId/like/:userIdPath Parameters:
postId: UUID of the postuserId: UUID of the user who liked the post
Response: 204 No Content
Error Responses:
400 Bad Request: Invalid UUID format forpostIdoruserId404 Not Found: Post not found
Example:
curl -X DELETE https://bp-social-network-app.onrender.com/api/v1/posts/5f5ff573-c092-4360-a629-d03bf2d583e8/like/660e8400-e29b-41d4-a716-446655440002Adds a comment to a post.
Request:
POST /posts/:postId/comment
Content-Type: application/jsonPath Parameters:
postId: UUID of the post to comment on
Request Body:
{
"userId": "660e8400-e29b-41d4-a716-446655440002",
"text": "Great post! Thanks for sharing."
}Validation Rules:
userId: Required, must be a valid UUIDtext: Required, 1-500 characters
Response: 201 Created
{
"id": "789e4567-e89b-12d3-a456-426614174003",
"postId": "7982bfb7-1780-4828-8ed6-278d468e08c6",
"userId": "660e8400-e29b-41d4-a716-446655440002",
"text": "Great post! Thanks for sharing.",
"createdAt": 1704067400000
}Error Responses:
400 Bad Request: Invalid UUID format or validation errors404 Not Found: Post not found
Example:
curl -X POST https://bp-social-network-app.onrender.com/api/v1/posts/5f5ff573-c092-4360-a629-d03bf2d583e8/comment \
-H 'Content-Type: application/json' \
-d '{"userId":"660e8400-e29b-41d4-a716-446655440002","text":"Great post!"}'Retrieves all comments for a specific post.
Request:
GET /posts/:postId/commentsPath Parameters:
postId: UUID of the post
Response: 200 OK
[
{
"id": "789e4567-e89b-12d3-a456-426614174003",
"postId": "7982bfb7-1780-4828-8ed6-278d468e08c6",
"userId": "660e8400-e29b-41d4-a716-446655440002",
"text": "Great post! Thanks for sharing.",
"createdAt": 1704067400000
},
{
"id": "889e4567-e89b-12d3-a456-426614174004",
"postId": "7982bfb7-1780-4828-8ed6-278d468e08c6",
"userId": "770e8400-e29b-41d4-a716-446655440003",
"text": "I totally agree!",
"createdAt": 1704067500000
}
]Error Responses:
400 Bad Request: Invalid UUID format forpostId404 Not Found: Post not found
Example:
curl https://bp-social-network-app.onrender.com/api/v1/posts/5f5ff573-c092-4360-a629-d03bf2d583e8/commentsAll error responses follow this format:
{
"error": "Error message describing what went wrong"
}HTTP Status Codes:
400 Bad Request: Validation errors or invalid UUID format404 Not Found: Resource not found (post, comment, etc.)409 Conflict: Business rule violation (e.g., duplicate like)500 Internal Server Error: Unexpected server errors
Run tests with:
npm testsrc/
application/ # services (business logic)
domain/ # models and validation
infrastructure/ # repos + errors
memory/ # in-memory repo implementations
server/ # express app
web/ # routes + middleware
