Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
Expand Down
4 changes: 3 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
PROJECT_NAME=mock-api-framework
SERVER_PORT=8000
USE_API_URL_PREFIX=api
USE_API_URL_PREFIX=api
LOG_REQUESTS=ON
DELETE_LOGS_ON_SERVER_RESTART=ON
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
node_modules
*.log
screenshots/
videos/
videos/
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The framework is written in TypeScript and can :
- Serve persisted mock data to the database
- Perform CRUD operations on the local database via a REST endpoint
- Mock API error codes/messages for testing frontend error handling logic
- Log and store API requests in JSON format and display information on the localhost:8000/logs route

## Set-up

Expand Down Expand Up @@ -64,7 +65,7 @@ A list of all endpoints can be viewed on http://localhost:8000/.

The project has been set-up with demo endpoints that can be removed or modified as needed.

![alt text](image.png)
![main server page](image.png)

### Useful Commands

Expand Down Expand Up @@ -297,6 +298,48 @@ this will return a 500 error code and the JSON response below:

```

## Logging

API request information and sent data can be logged and stored as JSON in the /src/logs/ folder.

Logs can be viewed at **localhost:8000/logs**.

![logging](logs.png)

### Set-up

To enable logging set the environment variables below in the .env

```js
LOG_REQUESTS = ON;
DELETE_LOGS_ON_SERVER_RESTART = ON;
```

You can choose to refresh the logfile every time the server restarts or persist the data by setting the DELETE_LOGS_ON_SERVER_RESTART variable.

To set up logging for a route, add the following to the api.ts file in the relevant handler, adjusting the request type (GET/POST/PUT/DELETE) and passing data to be logged as required:

```js
import logger from '../../utilities/logger';

function handler(pathName: string) {
return [
http.get(`/${pathName}`, ({ request }) => {

...
logger({
data: { <- extracted request body or query params data here -> },
pathName,
type: 'GET',
});
...
}),
]
}
```

**See the src/api/bikes api.ts file for an example of logging set up.**

## Customisation

### Changing api url prefix
Expand Down
41 changes: 41 additions & 0 deletions cypress/e2e/log-page-spec.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
describe('logging works expected information', () => {
it('log page works', () => {
cy.visit('/logs');
cy.get('h2').contains('API Requests Made');

cy.get('h3').contains(
'File can be viewed in /src/logs folder in container or local machine',
);
cy.get('h5').contains(
"LOG_REQUESTS env var must be set to 'ON' to log requests",
);
});

it('logging GET request works', () => {
cy.request('/api/bikes?type=ducati');
cy.visit('/logs');
cy.get('.json-container').should('exist');
cy.get('.json-container').contains('api/bikes');
cy.get('.json-container').contains('ducati');
cy.get('.json-container').contains('GET');
});

it('logging POST request works', () => {
cy.request('POST', '/api/bikes', {
name: 'kawasaki ninja',
type: 'kawasaki',
year: 2023,
color: 'red',
price: 20000,
});
cy.visit('/logs');
cy.get('.json-container').should('exist');
cy.get('.json-container').contains('api/bikes');
cy.get('.json-container').contains('kawasaki ninja');
cy.get('.json-container').contains('kawasaki');
cy.get('.json-container').contains('2023');
cy.get('.json-container').contains('red');
cy.get('.json-container').contains('20000');
cy.get('.json-container').contains('POST');
});
});
Binary file added logs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"highlight.js": "^11.11.1",
"markdown-it": "^14.1.0",
"msw": "^2.7.4",
"pretty-print-json": "^3.0.4",
"tsx": "^4.19.3",
"typescript": "^5.8.3",
"zod": "^3.24.2"
Expand Down
17 changes: 17 additions & 0 deletions src/api/bikes/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { http, HttpResponse } from 'msw';
import logger from '../../utilities/logger';

// Add any http handler here (get, push , delete etc., and middleware as needed)

Expand All @@ -9,13 +10,29 @@ function handler(pathName: string) {
const type = url.searchParams.get('type');
console.log(`starting ${pathName}`);
console.log('Item Type is', type);

// Log the request passing the request data, pathName and request type to the logger function
logger({
data: { type },
pathName,
type: 'GET',
});

return HttpResponse.json({
response: `this is a GET test response from ${pathName} for bike type: ${type ?? 'none'}`,
});
}),
http.post(`/${pathName}`, async ({ request }) => {
// Get Body Data using json(), text() or formData() depending on what is sent
const bodyData = await request.json();

// Log the request passing the request data, pathName and extra information to the logger function
logger({
data: bodyData,
type: 'POST',
pathName,
});

return HttpResponse.json({
response: `this is a POST test response from ${pathName} with bodyData ${JSON.stringify(bodyData)}`,
});
Expand Down
Empty file added src/logs/.gitkeep
Empty file.
14 changes: 13 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,24 @@ import { createServer } from '@mswjs/http-middleware';
import * as seeders from './seeders/index.js';
import getApiRoutes from './utilities/file-scan.js';
import serverPage from './utilities/server-page.js';
import logPage from './utilities/log-page.js';
import { deleteLogs } from './utilities/logger.js';
import { env } from './utilities/env.js';

const { apiHandlers, apiRoutes } = await getApiRoutes();

const httpServer = createServer(...apiHandlers, ...serverPage(apiRoutes));
const httpServer = createServer(
...apiHandlers,
...serverPage(apiRoutes),
...logPage(),
);

// Delete any logs on server start if the DELETE_LOGS_ON_SERVER_RESTART env var is set to 'ON'
if (process.env?.DELETE_LOGS_ON_SERVER_RESTART?.toUpperCase() === 'ON') {
deleteLogs();
}

// Set up the server to listen on the specified port
httpServer.listen(env.SERVER_PORT);

// Execute dB seeder functions
Expand Down
107 changes: 107 additions & 0 deletions src/utilities/log-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { http, HttpResponse } from 'msw';
import { prettyPrintJson } from 'pretty-print-json';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const createHtml = () => {
function readLogs() {
const logFolder = `${__dirname}/../logs`;
const logPath = path.join(logFolder, 'api_request_log.log');
let logs = '';
// Append the log entry to the log file
try {
logs = fs.readFileSync(logPath, 'utf8');
} catch {
logs =
'{"message": "No logs found", "solution": "Set LOG_REQUESTS env var to ON and add a logger function to a route api.ts. Restart the server then retry the request"}';
}

return `[${logs}]`;
}

const htmlString = `
<html>
<header>
<title>API Request Log</title>
</header>
<body style="margin: 0px; background-color: #00200B; display: flex; flex-direction: column; justify-content: center; align-items: center; min-height:100vh; font-family: sans-serif;">
<h2 style="color:white" data-cy="logger-title">API Requests Made</h2>
<h3 style="color:white">File can be viewed in /src/logs folder in container or local machine</h3>
<h5 style="color:grey; margin-top:-10px;">LOG_REQUESTS env var must be set to 'ON' to log requests</h5>
<div class="json-container" style="width: 100%; padding:20px; box-sizing: border-box;">
${prettyPrintJson.toHtml(JSON.parse(readLogs()), { indent: 4, lineNumbers: true })}
</div>

</body>
<style>

.highlight {
background-color:#28831C;
padding:5px 10px 5px 10px;
border-radius: 5px;
font-weight: bold;
color: white;
margin-left: 15px;
border: 3px white solid;
}

.info {
font-weight: bold;
padding: 10px 0px 10px 0px;
}
.logs {
max-width: 400px;
}


/* Layout */
.json-container { font-family: menlo, consolas, monospace; font-style: normal; font-weight: bold; line-height: 1.4em; font-size: 0.9rem; transition: background-color 400ms; display: block }
a.json-link { text-decoration: none; border-bottom: 1px solid; outline: none; }
a.json-link:hover { background-color: transparent; outline: none; }
ol.json-lines { white-space: normal; padding-inline-start: 3em; margin: 0px; max-width:100%; overflow: hidden;}
ol.json-lines >li { white-space: pre-wrap; text-indent: 0.7em; line-height: 1.5em; padding: 0px;}
ol.json-lines >li::marker { font-family: system-ui, sans-serif; font-weight: normal; }
.json-key, .json-string, .json-number, .json-boolean, .json-null, .json-mark, a.json-link, ol.json-lines >li { transition: all 400ms; }


/* Dark Mode */
.json-container { background-color: #00200B; }
.json-key { color: indianred; }
.json-string { color: khaki}
.json-number { color: deepskyblue; }
.json-boolean { color: mediumseagreen;}
.json-null { color: darkorange; }
.json-mark { color: silver; }
a.json-link { color: mediumorchid; }
a.json-link:visited { color: slategray; }
a.json-link:hover { color: violet; }
a.json-link:active { color: slategray; }
ol.json-lines >li::marker { color: silver; }
ol.json-lines >li:nth-child(odd) { background-color: #293829; }
ol.json-lines >li:nth-child(even) { background-color: #354135; }
ol.json-lines >li:hover { background-color: dimgray; }
</style>
</html>
`;

return htmlString;
};

const logPage = () => {
return [
http.get(`/logs`, () => {
return new HttpResponse(createHtml(), {
status: 200,
headers: {
'Content-Type': 'text/html',
},
});
}),
];
};

export default logPage;
59 changes: 59 additions & 0 deletions src/utilities/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { type DefaultBodyType } from 'msw';

type LogData = {
data: DefaultBodyType;
type?: string;
pathName: string;
};

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

/**
* Logs API requests to a file into the logs folder
* @param {DefaultBodyType} LogData.data - data sent with the request as an object E.g {id: 101, userId: 1, title: 'title', body: 'body'}
* @param {string} [LogData.type] - type of request (GET, POST, PUT, DELETE)
* @param {string} [LogData.pathName] - path of the request
*/
function logger({
data = { error: 'no data provided' },
type = 'GET',
pathName = 'no path provided',
}: LogData) {
const logFolder = `${__dirname}/../logs`;
const logPath = path.join(logFolder, 'api_request_log.log');

if (process.env?.LOG_REQUESTS?.toUpperCase() !== 'ON') return;
console.log(
`New API Request:${new Date().toLocaleString()}. Request data viewable in browser 'localhost:${process.env?.SERVER_PORT ?? '8000'}/logs' or in the 'logs/ folder'`,
);

// Convert the object to a string
let logEntry = `{"request_information":{"path": "${pathName}","request_type":"${type}", "request_time":"${new Date().toLocaleString()}"},"sent_data":${JSON.stringify(
data,
)
.trim()
.replaceAll('\\n', '')
.replaceAll(/\s{2,}/g, ' ')}}`;

if (fs.existsSync(logPath)) {
// Add separating comma if NOT the first entry to make valid JSON
logEntry = `,${logEntry}`;
}

// Create the logs folder if it doesn't exist otherwise append to the log file
fs.writeFileSync(logPath, logEntry, { flag: 'a+' });
}

// Use node fs to delete the log file
export function deleteLogs() {
const logFolder = `${__dirname}/../logs`;
const logPath = path.join(logFolder, 'api_request_log.log');
if (!fs.existsSync(logPath)) return;
fs.unlinkSync(logPath);
}

export default logger;
2 changes: 2 additions & 0 deletions src/utilities/server-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const homePage = (apiPaths: string[]) => {

</div>

<h3 class="info">Logs URL: <a class="highlight" href="/logs">localhost:${process.env?.SERVER_PORT}/logs</a> </h3>

<p class="starred"><small>* Add new api endpoints to the api folder. <br/>For media endpoints include the media name in the url E.g /images/placeholder.png </small></p>


Expand Down