Welcome to the Notes App Backend. Powered by Node.js, Express.js, Mongoose, and Docker, this API provides a simple way to manage notes & is designed to be readable and easy to use as a template for future APIs.
git clone https://github.com/wadedesir/notes-app.git
- Make an .env file and set the
MONGODB_URI
environment variable. - Navigate to the backend directory:
cd backend
- Install dependencies:
npm install
- Run the backend:
npm run dev
- Navigate to the backend directory:
cd backend
- Run the backend:
docker compose up
- Your API is now running at http://localhost:8420
The backend follows a modular architecture, with distinct components responsible for handling different aspects of the application logic.
├── index.js // Main entry point into the app
├── routes // Express routes
│ ├── LoginRouter.js
│ ├── UserRouter.js
│ ├── NoteRouter.js
├── controllers // Application logic implementations
│ ├── NoteController.js
│ ├── UserController.js
│ ├── LoginController.js
├── models // Database object interfaces
│ ├── User.js
│ ├── Note.js
├── util // Utility functions
│ ├── db_util.js
│ ├── logger.js
│ ├── config.js
│ ├── middleware.js
├── tests // Jest tests (automatically run through GitHub Actions)
- LoginRouter.js: Defines routes related to user authentication.
- UserRouter.js: Handles CRUD operations for user management.
- NoteRouter.js: Manages CRUD operations for notes.
- LoginController.js: Contains logic for user authentication.
- UserController.js: Implements methods for user CRUD operations.
- NoteController.js: Implements methods for note CRUD operations.
- User.js: Defines the schema and methods for interacting with user data in the database.
- Note.js: Defines the schema and methods for interacting with note data in the database.
- db_util.js: Provides utility functions for interacting with the database.
- logger.js: Handles logging throughout the application.
- config.js: Manages application configuration settings.
- middleware.js: Contains middleware functions for request processing.
The following outlines the various responses from the API.
Method | Path | Description |
---|---|---|
GET | /v1/users | Get all users |
POST | /v1/users | Create a new user |
GET | /v1/users/{id} | Get user by ID |
PUT | /v1/users/{id} | Update user by ID |
DELETE | /v1/users/{id} | Delete user by ID |
GET | /v1/notes | Get all notes |
POST | /v1/notes | Create a new note |
GET | /v1/notes/{id} | Get note by ID |
PUT | /v1/notes/{id} | Update note by ID |
DELETE | /v1/notes/{id} | Delete note by ID |
POST | /v1/login | Authenticate user |
Name | Path | Description |
---|---|---|
User | #/components/schemas/User | 🥷🏾 User Schema |
UserCreate | #/components/schemas/UserCreate | 🚪 User Creation Schema |
UserUpdate | #/components/schemas/UserUpdate | 🛠️ User Update Schema |
Note | #/components/schemas/Note | 📝 Note Schema |
NoteCreate | #/components/schemas/NoteCreate | 📝 Note Creation Schema |
NoteUpdate | #/components/schemas/NoteUpdate | 🛠️ Note Update Schema |
LoginCredentials | #/components/schemas/LoginCredentials | 🔑 Login Credentials Schema |
LoginResponse | #/components/schemas/LoginResponse | 🔑 Login Response Schema |
- Summary: Get all users
- 200 OK: A list of users.
[
{
"id": "string",
"name": "string",
"username": "string"
}
]
- Summary: Create a new user
- application/json
{
"name": "string",
"username": "string",
"password": "string"
}
- 201 Created: The created user.
{
"id": "string",
"name": "string",
"username": "string"
}
- 400 Bad Request.
- Summary: Get user by ID
- 200 OK: The user.
{
"id": "string",
"name": "string",
"username": "string"
}
- 404 Not Found.
- Summary: Update user by ID
- application/json
{
"name": "string",
"username": "string",
"password": "string"
}
- 200 OK: The updated user.
{
"id": "string",
"name": "string",
"username": "string"
}
- 404 Not Found.
- Summary: Delete user by ID
-
204 No Content: User deleted successfully.
-
404 Not Found.
- Summary: Get all notes
- 200 OK: A list of notes.
[
{
"id": "string",
"content": "string",
"important": true,
"user": {
"id": "string",
"name": "string",
"username": "string"
}
}
]
- Summary: Create a new note
- application/json
{
"content": "string",
"important": true
}
- 201 Created: The created note.
{
"id": "string",
"content": "string",
"important": true,
"user": {
"id": "string",
"name": "string",
"username": "string"
}
}
-
400 Bad Request.
-
401 Unauthorized.
- Summary: Get note by ID
- 200 OK: The note.
{
"id": "string",
"content": "string",
"important": true,
"user": {
"id": "string",
"name": "string",
"username": "string"
}
}
- 404 Not Found.
- Summary: Update note by ID
- application/json
{
"content": "string",
"important": true
}
- 200 OK: The updated note.
{
"id": "string",
"content": "string",
"important": true,
"user": {
"id": "string",
"name": "string",
"username": "string"
}
}
- 404 Not Found.
- Summary: Delete note by ID
-
204 No Content: Note deleted successfully.
-
404 Not Found.
- Summary: Authenticate user
- application/json
{
"username": "string",
"password": "string"
}
- 200 OK: Login successful.
{
"token": "string"
}
-
400 Bad Request.
-
401 Unauthorized.
{
"id": "string",
"name": "string",
"username": "string"
}
{
"name": "string",
"username": "string",
"password": "string"
}
{
"name": "string",
"username": "string",
"password": "string"
}
{
"id": "string",
"content": "string",
"important": true,
"user": {
"id": "string",
"name": "string",
"username": "string"
}
}
{
"content": "string",
"important": true
}
{
"content": "string",
"important": true
}
{
"username": "string",
"password": "string"
}
{
"token": "string"
}
Whenever a new PR is made, tests located in /backend/tests
are run automatically through GitHub Actions (check out the .github/workflows folder at the root of the repo).
The backend API uses eslint to enforce good code style & perform static code analysis. The code style in use is the 'standard' preset with no extra rules.
To run linting tests locally:
- Navigate to the backend directory:
cd backend
- Run linting:
npm run lint
Note: You can run npx eslint --fix .
while in the backend directory to automatically fix any linting problems.
To run the app's unit and integration tests, follow these steps:
- If you don't have Docker installed:
npm test
- If you have Docker installed:
docker compose run api npm run test
Integration tests for the notes API are handled through backend/test/note_api.test.js
. This suite tests the application logic of the API to ensure correct behavior and that we're getting the data we expect.
We use super test for the backend API testing. The test imports the Express app from the main module (index.js) and wraps it with the supertest function into a so-called superagent object. We use this superagent object to make our test API requests.
We define some setup & teardown logic for jest in setup.js & teardown.js. What we're doing here is making jest a global variable in setup.js
, and making sure our process ends with exit code 0 in teardown.js
which tells the 'shell' running our test command that everything went fine.
We're using the User & Note model in a top level beforeAll
function to wipe the User & Note collection data, so we're not relying on the databases previous state (which could introduce false positives & other issues to our tests). This beforeAll
logic will run once before all the other tests in this file.
notes-app/backend/tests/note_api.test.js
Lines 16 to 19 in da9820c
A GET request to /v1/users/
should return all the users, so when there are some initial users (after we POST a test user), we should be able to see them when we hit the /v1/users/
endpoint with a GET request.
What we're doing here is hitting the /v1/users/
endpoint and mapping the returned object array into a new object array that just has the name for each object. Then we step through that array with jest expects(contents).toContain(testUserName)
to make sure it contains the user name for the test user we just created.
notes-app/backend/tests/note_api.test.js
Lines 21 to 35 in da9820c
When no users are present, we test that we can actually create a user. We create a mock user object and post it to the /v1/users/
endpoint, which should create a new user and respond back with 202 & the data for the new user.
notes-app/backend/tests/note_api.test.js
Lines 37 to 47 in da9820c
When a user has been added, we test that we can update the database with the user's data by using the HTTP PUT method.
notes-app/backend/tests/note_api.test.js
Lines 86 to 96 in 7400dcd
After all our tests are done running, we delete the user we used for our tests.
When we're logged in, we should be able to retrieve all the notes. We hit the /v1/notes
end point and check that it returns a 200 response.
notes-app/backend/tests/note_api.test.js
Lines 78 to 84 in da9820c
When we're logged in, we should be able to create a new note. The user token from when we logged in got saved to the test's top level scope, so it's available in all our test cases. We use the token here to make a POST request to the notes endpoint, and then we check the status code to make sure everything came back as expected.
notes-app/backend/tests/note_api.test.js
Lines 86 to 100 in da9820c
In this test, we're trying to make sure that we get the correct note by ID. We first grab all the notes in the database directly through our Note Model with notesInDb()
. After we get all the notes, we grab the first note in the collection and use its ID in the request to the GET /v1/notes/${ID}
endpoint.
notes-app/backend/tests/note_api.test.js
Lines 123 to 135 in da9820c
In this test, we're trying to make sure that we delete the right note. We first grab all the notes in the database directly through our Note Model with notesInDb()
. After we get all the notes, we grab the first note in the collection and use its ID in the request to the DELETE /v1/notes/${ID}
endpoint.
notes-app/backend/tests/note_api.test.js
Lines 194 to 213 in da9820c
In this test, we first check to make sure we can't log in with invalid credentials. We send invalid credentials on purpose and check to see if we get back a 401 unauthorized request. After making sure we can't log in with invalid credentials, we use correct information to log in. We post to the same endpoint & check to see if its response is 200. Then we set the user token & user ID to variables.
notes-app/backend/tests/note_api.test.js
Lines 49 to 76 in da9820c
The unit tests for the both the notes and user apis are found at backend/test/note_api_unit.test.js
, backend/test/user_unit.test.js
, and backend/test/getTokenFrom.test.js
. These suites will test the logic of the API to make sure our APIs are behaving as expected.
The unit tests in user_unit.test.js
test the findUserById
api. You may notice these tests look different than the others.
That is because this suite includes an experiemental mocking module from Jest that tests ECMAScript Modules. In this application, we are using static import declaration which is a feature of ECMAScript as opposed to importing using require() which is a feature of CommonJS. ESM evaluates static import
statements before looking at code however jest mocks must be set prior to importing the module that is being mocked. Therefore the hoisting of jest.mock
calls that happen in CJS won't work for ESM.
Let's break down the following test: https://github.com/wadedesir/notes-app/blob/main/backend/tests/user_unit.test.js#L3-L50
- In this test, we are executing an asynchronous setup function
beforeAll(async () => {
})
- We then use the
jest.unstable_mockModule
to mock ourUser
module to be used in our test. Since ES6 modules are hoisted, we need to set our mocks, prior to running code that uses the User model
jest.unstable_mockModule('../models/User', () => ({...})
- Here we are insuring that the default export of the
User
module is imported
jest.unstable_mockModule('../models/User', () => ({
default:{...}}))
- Here we are mocking the
findById()
function so that our mock user has a mockfindById()
method as it does in theUser
module
...{
findById: jest.fn().mockImplementation(id => {...}
- In the UserController, we call
User.findById()
and it can resolve to a value thatfindUserById
uses for two different branches: user found or user not found.
- In our mock, we are using
Promise.resolve
to immediately return mock values that simulate each pathway. One where our user's id is in the DB, so we return the user. The other where the user is not found andnull
is returned. Note that since the id is the only data needed for the scope of these tests, the complete user data that the query may return is not mocked.
...{
if (id === '123456789') {
return Promise.resolve({
_id: '123456789'})
} else {
return Promise.resolve(null)}
}
- Once the mock is set, we can dynamically import the UserController. Since the mock User was set prior to this import, our imported api can utilize the mock User in the test environment
findUserById = (await import('../controllers/UserController')).findUserById
- Here we are testing the happy pathway where a user is present in our DB. We are mocking a user that we want to query our db for. We are also mocking the express
req
HTTP request object andres
HTTP response object as we are not actually making the api call to the database. We then callfindUserById
using the value ofreq.params.id
to similulate the call =>User.findById(req.params.id)
. In this test, we expect that a user is found andres.json
will be called with the correct 'user' from the database
test('when valid ID return user ', async () => {
const mockUser = { _id: '123456789' }
const req = {
params: { id: '123456789' }
}
const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis()
}
await findUserById(req, res)
expect(res.json).toHaveBeenCalledWith(mockUser)
})
- The second test, we are testing the other scenario, where our database does not have the user and
findUserById
responds with the appropriate error message and status. In this test, both assertions must be true in order for the test to pass
test('when user id is not found, return 404', async () => {
const req = {
params: { id: 'ID NOT Found IN DB' }
}
const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis()
}
await findUserById(req, res)
expect(res.json).toHaveBeenCalledWith({
error: 'user with id:ID NOT Found IN DB not found'
})
expect(res.status).toHaveBeenCalledWith(404)
})