A software project designed to implement a Secret Note API
A software project designed to implement a Secret Note API, featuring a common CRUD pattern that allows users to securely create, read, update, and delete encrypted notes. This API is built using NestJS and supports encrypted data storage, ensuring that each note remains confidential and is only accessible in its decrypted form through secure API endpoints.
Dear Reviewers,
I have documented some of my decisions and approaches in the project. To expedite the process, I focused on adding tests for the add functionality of the API. Additionally, I have included contexts regarding Git usage, instructions on how to run the software, and potential improvements if more time were available.
Please review the documentation in detail. I look forward to discussing this application with you and receiving your feedback.
$ pnpm install
# development
$ pnpm run start
# watch mode
$ pnpm run start:dev
# production mode
$ pnpm run start:prod
If you want to run it without Docker please pay attention about the MongoDB configurations. You can see it on the .env.example file.
You also can run the application by running the Docker container. To do this you just need to run the command below:
# --build flag just for the first time!
$ docker-compose up --build
# unit tests
$ pnpm run test
# e2e tests
$ pnpm run test:e2e
# unit tests
$ pnpm run test:unit
# test coverage
$ pnpm run test:cov
contexts # To keep different contexts like note
shared # To keep different parts which could be used in all parts of the application
domain # To keep shared domain parts like exceptions
infrastructure # To keep shared infrastructure parts like database, interceptors, pipes, etc
contexts # To keep different contexts like note
shared # To keep different parts which are shared in the contexts
Each context has its own application, infrastructure, and domain directory to keep different parts of that specific context like mappers, controllers, usecases, etc.
There are two different branches. The first branch is production, the second branch is development. I made feature branch locally which will be added to the development branch with all commits(linier) and from the development branch will be merged into production branch for each separated functionality which contains all commits in single commit(squash). In this way we will have a more cleaner production branch so if something gets wrong in the production we are simply able to revert to the last version with one single revert command.
I didn't add a demo branch because I think these two branches are enough for a simple code challenge.
This project implements a secure API for managing secret notes. The system ensures that notes are encrypted before being stored in a MongoDB database and decrypted when retrieved(depends to the entry request). The API follows a clean architecture, separating concerns into controllers, services, and use cases.
Purpose: Handles incoming HTTP requests for creating secret notes.
Key Features:
- Uses
ZodValidationPipe
for validating incoming data against theCreateSecretNoteDto
. - Maps exceptions to HTTP responses, providing meaningful error information to the client.
Purpose: Implements the application logic for creating a secret note.
Responsibilities:
- Coordinates with
SecretNoteService
to handle the creation of a note. - Manages application-level exceptions and rethrows them with contextual information.
Purpose: Manages interactions with the MongoDB database and encrypts notes before saving.
Key Operations:
- Validates and parses DTOs using Zod.
- Encrypts the note data.
- Maps DTOs to database models using
SecretNoteMapper
. - Saves encrypted notes to MongoDB.
Purpose: Validates incoming data structures to ensure they meet the API's expectations before processing.
Implementation:
- Integrated through a custom
ZodValidationPipe
which is applied at the controller level to check and parse incomingCreateSecretNoteDto
objects.
Purpose: Used for validating and transforming incoming request data before it reaches the controller handler.
Setup: Applied at the route handler level using @UsePipes()
decorator.
This project adopts a robust testing strategy that includes both unit tests and end-to-end (E2E) tests, ensuring that both individual components function as expected and that the system works as a whole from an end user’s perspective.
-
Purpose: Unit tests are designed to test individual pieces of code in isolation, primarily focusing on small functions or modules. The goal is to ensure that each part performs as expected independently of others.
-
Location: Unit tests are located next to their respective source files in the src directory, following the convention of naming test files with a .spec.ts suffix. This proximity helps in maintaining and navigating related source and test files.
Example Structure:
src/
├── contexts/
├── secret-notes/
├── domain/
├── services/
├── secret-note.service.ts
├── secret-note.service.spec.ts
Key Technologies:
These tests leverage Jest as the testing framework, utilizing features such as mocks and spies to isolate dependencies.
-
Purpose: E2E tests verify the system’s behavior from start to finish. They are intended to simulate user behavior and interactions to ensure all integrated parts of the application work together correctly.
-
Location: E2E tests are maintained in the test directory at the root of the project. This separation from unit tests helps in distinguishing between testing scopes and managing dependencies specific to E2E testing.
test/
├── contexts/
├── secret-notes/
├── application
├── secret-note.controller.spec.ts
Key Technologies:
E2E tests use Jest alongside NestJS Testing Utilities and often interact with the full application, databases, and other services. They may include setup scripts for environment simulation, such as database seeding or mock external services.
-
Unit Tests: Run with
pnpm run test:unit
, which executes all .spec.ts files across the src directory. -
E2E Tests: Executed separately using npm run
pnpm run test:e2e
to avoid interference with unit tests and to handle potentially different setup requirements, such as environment configurations or database handling.
- Continuous Integration: Both unit and E2E tests are integrated into the CI/CD pipeline to ensure tests are automatically run in different environments, preventing regressions.
- Code Coverage: Strive for high code coverage in unit tests while ensuring E2E tests cover critical user journeys and edge cases.
- Mocking and Isolation: Critical for unit testing to ensure no external changes affect the outcomes. E2E tests, however, should interact with real implementations as much as possible.
- To add a new note.
Here is an example of how to interact with the server:
{
"id": "102320",
"title": "My Secret Note",
"note": "This is a secret note.",
"userId": "user123"
}
As the response you have to get a 201 HTTP response status code with the body of the result like below:
{
"note": "b7b3994e035104cc71788b75e865b37507779b8f94be221bd2219a0c05248a34",
"id": "102320",
"title": "My Secret Note",
"tags": [],
"userId": "user123",
"isEncrypted": true,
"version": 1,
"_id": "6672e58f4164202c8c7dee1e",
"createdAt": "2024-06-19T14:05:03.426Z",
"updatedAt": "2024-06-19T14:05:03.426Z",
"__v": 0
}
- Just a request to the server.
Here is an example of the expected response:
[
{
"id": "12345",
"title": "My Secret Note",
"userId": "user123",
"createdAt": "2024-06-19T14:20:11.775Z"
},
{
"id": "123466",
"title": "Another Note",
"userId": "user123",
"createdAt": "2024-06-19T14:21:15.005Z"
},
...
]
- Send a request to
/secret-notes/{id}
likehttp://localhost:3000/secret-notes/123466
Here is an example of the expected response:
{
"note": "This is a secret note.",
"id": "123466",
"title": "My Secret Note",
"tags": [],
"userId": "user123",
"isEncrypted": true,
"version": 1,
"createdAt": "2024-06-17T22:29:15.005Z",
"updatedAt": "2024-06-17T22:29:15.005Z"
}
- Send a request to
/secret-notes/{id}?encrypted=true
likehttp://localhost:3000/secret-notes/123466?encrypted=true
Here is an example of the expected response:
{
"note": "b7b3994e035104cc71788b75e865b37507779b8f94be221bd2219a0c05248a34",
"id": "123466",
"title": "My Secret Note",
"tags": [],
"userId": "user123",
"isEncrypted": true,
"version": 1,
"createdAt": "2024-06-17T22:29:15.005Z",
"updatedAt": "2024-06-17T22:29:15.005Z"
}
- Send a request to
/secret-notes/{id}
likehttp://localhost:3000/secret-notes/123466
Here is an example of the expected request and response:
- RQ :
{
"note": "This is an updated secret note.",
"title": "My Secret Note title",
"version": 2
}
- RS :
{
"success": true,
"message": "Updated has been done successfully.",
"note": {
"note": "This is an updated secret note.",
"id": "123466",
"title": "My Secret Note title",
"tags": [],
"userId": "user123",
"isEncrypted": true,
"version": 2,
"createdAt": "2024-06-17T22:29:15.005Z",
"updatedAt": "2024-06-19T17:08:24.156Z"
}
- Send a request to
/secret-notes/{id}
likehttp://localhost:3000/secret-notes/123466
Here is an example of the expected response:
{
"success": true,
"message": "Note deleted successfully"
}
- more unit tests for get, update and delete requests
- more e2e tests for get, update and delete requests
- A better error handler for converting domain exceptions to HTTP exceptions
- Adding a logger to log RQ and RS in DB or somewhere else like DD
Nest is MIT licensed.