diff --git a/README.md b/README.md index de20ac8..762528a 100644 --- a/README.md +++ b/README.md @@ -86,14 +86,14 @@ npm run dev The framework is written in TypeScript and can : -- Serve data from static files (JSON or text) -- Serve media : Images and Videos -- Serve markdown files -- Be used to write and test AWS Lambda Functions -- Use custom middleware to transform input/output -- Serve random Database seed data -- Serve persisted mock data to the database -- Perform CRUD operations on the local database via a REST endpoint +- Serve data from static files (JSON or text) +- Serve media : Images and Videos +- Serve markdown files +- Be used to write and test AWS Lambda Functions +- Use custom middleware to transform input/output +- Serve random Database seed data +- Serve persisted mock data to the database +- Perform CRUD operations on the local database via a REST endpoint ### Setting up a new API route @@ -111,7 +111,7 @@ mkdir src/api/users #### IMPORTANT -Rember to use 'npm run dev' to start a local dev server when developing new API endpoints. +Remember to use 'npm run dev' to start a local dev server when developing new API endpoints. Once endpoints have been developed and tested then use 'npm run start' to build the docker image and start a container to serve the endpoints on localhost (or use 'npm run rebuild' or 'npm run nuke' if changes need to be made to an existing docker image). @@ -271,6 +271,31 @@ http://localhost:8000/api/json/demo The templates directory contains some templates for different type of handlers, models and seeders but the demo api endpoints can just as easily be copied and modified for individual use cases. +### 13. Error Route + +It can be useful to mock api errors in order to test frontend error handling logic. + +To do this redirect frontend fetch requests to the api/error route. + +``` +http://localhost:8000/api/error +``` + +The default error for this route is a '404: not found' error but if specific errors are required, then this can be customised by passing 'status' to the endpoint and also a custom 'message' if required. + +E.g to mimic a 500 'Internal Server Error' + +``` +http://localhost:8000/api/error?status=500&message=Internal%20Server%20Error +``` + +this will return a 500 error code and the JSON response below: + +``` +{"error":"500: Internal Server Error"} + +``` + ## Customisation ### Changing api url prefix diff --git a/cypress/e2e/error-endpoint-spec.cy.ts b/cypress/e2e/error-endpoint-spec.cy.ts new file mode 100644 index 0000000..a2a8dd1 --- /dev/null +++ b/cypress/e2e/error-endpoint-spec.cy.ts @@ -0,0 +1,24 @@ +describe('default mock error endpoint works as expected', () => { + it('checks default error endpoint is running', () => { + cy.request({ url: '/api/error', failOnStatusCode: false }).then( + (response) => { + expect(response.status).to.eq(404); + expect(response.body).to.be.jsonSchema({ + error: '404: Not Found', + }); + }, + ); + }); + + it('checks default error endpoint is running', () => { + cy.request({ + url: '/api/error?status=500&message=Internal%20Server%20Error', + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(500); + expect(response.body).to.be.jsonSchema({ + error: '500: Internal Server Error', + }); + }); + }); +}); diff --git a/cypress/e2e/server-page-spec.cy.ts b/cypress/e2e/server-page-spec.cy.ts index 4d7152f..8391716 100644 --- a/cypress/e2e/server-page-spec.cy.ts +++ b/cypress/e2e/server-page-spec.cy.ts @@ -10,6 +10,7 @@ describe('Server page contains expected information', () => { '/api/posts', '/api/users', '/api/videos', + '/api/error', ]; cy.visit('/'); cy.get('h1').contains('Running'); diff --git a/package-lock.json b/package-lock.json index 3e7053c..ad49457 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,31 +4,30 @@ "requires": true, "packages": { "": { - "name": "mock-api-framework-template", "dependencies": { - "@faker-js/faker": "^9.6.0", + "@faker-js/faker": "^9.7.0", "@mswjs/data": "^0.16.2", "@mswjs/http-middleware": "^0.10.3", "@types/chai-json-schema": "^1.4.10", "@types/express": "^5.0.1", "@types/markdown-it": "^14.1.2", "chai-json-schema": "^1.5.1", - "dotenv": "^16.4.7", + "dotenv": "^16.5.0", "highlight.js": "^11.11.1", "markdown-it": "^14.1.0", - "msw": "^2.7.3", + "msw": "^2.7.4", "tsx": "^4.19.3", - "typescript": "^5.8.2", + "typescript": "^5.8.3", "zod": "^3.24.2" }, "devDependencies": { "@commitlint/cli": "^19.8.0", "@commitlint/config-conventional": "^19.8.0", - "@types/aws-lambda": "^8.10.148", - "@types/node": "^22.13.13", - "cypress": "^14.2.0", + "@types/aws-lambda": "^8.10.149", + "@types/node": "^22.14.1", + "cypress": "^14.3.0", "husky": "^9.1.7", - "lint-staged": "^15.5.0", + "lint-staged": "^15.5.1", "prettier": "3.5.3", "start-server-and-test": "^2.0.11", "xo": "^0.60.0" @@ -1177,9 +1176,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.6.0.tgz", - "integrity": "sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw==", + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.7.0.tgz", + "integrity": "sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==", "funding": [ { "type": "opencollective", @@ -1592,9 +1591,9 @@ } }, "node_modules/@types/aws-lambda": { - "version": "8.10.148", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.148.tgz", - "integrity": "sha512-JL+2cfkY9ODQeE06hOxSFNkafjNk4JRBgY837kpoq1GHDttq2U3BA9IzKOWxS4DLjKoymGB4i9uBrlCkjUl1yg==", + "version": "8.10.149", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.149.tgz", + "integrity": "sha512-NXSZIhfJjnXqJgtS7IwutqIF/SOy1Wz5Px4gUY1RWITp3AYTyuJS4xaXr/bIJY1v15XMzrJ5soGnPM+7uigZjA==", "dev": true, "license": "MIT" }, @@ -1754,12 +1753,12 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/node": { - "version": "22.13.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", - "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/normalize-package-data": { @@ -3719,14 +3718,14 @@ } }, "node_modules/cypress": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.2.0.tgz", - "integrity": "sha512-u7fuc9JEpSYLOdu8mzZDZ/JWsHUzR5pc8i1TeSqMz/bafXp+6IweMAeyphsEJ6/13qbB6nwTEY1m+GUAp6GqCQ==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.3.0.tgz", + "integrity": "sha512-rRfPl9Z0/CczuYybBEoLbDVuT1OGkhYaJ0+urRCshgiDRz6QnoA0KQIQnPx7MJ3zy+VCsbUU1pV74n+6cbJEdg==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "@cypress/request": "^3.0.7", + "@cypress/request": "^3.0.8", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -3763,7 +3762,7 @@ "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.5.3", + "semver": "^7.7.1", "supports-color": "^8.1.1", "tmp": "~0.2.3", "tree-kill": "1.2.2", @@ -4331,9 +4330,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -8084,9 +8083,9 @@ } }, "node_modules/lint-staged": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.0.tgz", - "integrity": "sha512-WyCzSbfYGhK7cU+UuDDkzUiytbfbi0ZdPy2orwtM75P3WTtQBzmG40cCxIa8Ii2+XjfxzLH6Be46tUfWS85Xfg==", + "version": "15.5.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.1.tgz", + "integrity": "sha512-6m7u8mue4Xn6wK6gZvSCQwBvMBR36xfY24nF5bMTf2MHDYG6S3yhJuOgdYVw99hsjyDt2d4z168b3naI8+NWtQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8901,9 +8900,9 @@ "license": "MIT" }, "node_modules/msw": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.7.3.tgz", - "integrity": "sha512-+mycXv8l2fEAjFZ5sjrtjJDmm2ceKGjrNbBr1durRg6VkU9fNUE/gsmQ51hWbHqs+l35W1iM+ZsmOD9Fd6lspw==", + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.7.4.tgz", + "integrity": "sha512-A2kuMopOjAjNEYkn0AnB1uj+x7oBjLIunFk7Ud4icEnVWFf6iBekn8oXW4zIwcpfEdWP9sLqyVaHVzneWoGEww==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -10397,9 +10396,9 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -11551,9 +11550,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -11598,9 +11597,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/unicorn-magic": { diff --git a/package.json b/package.json index 6645e45..2bf2910 100644 --- a/package.json +++ b/package.json @@ -18,29 +18,29 @@ "validate-branch-name": "bash validate-branch-name.sh" }, "dependencies": { - "@faker-js/faker": "^9.6.0", + "@faker-js/faker": "^9.7.0", "@mswjs/data": "^0.16.2", "@mswjs/http-middleware": "^0.10.3", "@types/chai-json-schema": "^1.4.10", "@types/express": "^5.0.1", "@types/markdown-it": "^14.1.2", "chai-json-schema": "^1.5.1", - "dotenv": "^16.4.7", + "dotenv": "^16.5.0", "highlight.js": "^11.11.1", "markdown-it": "^14.1.0", - "msw": "^2.7.3", + "msw": "^2.7.4", "tsx": "^4.19.3", - "typescript": "^5.8.2", + "typescript": "^5.8.3", "zod": "^3.24.2" }, "devDependencies": { "@commitlint/cli": "^19.8.0", "@commitlint/config-conventional": "^19.8.0", - "@types/aws-lambda": "^8.10.148", - "@types/node": "^22.13.13", - "cypress": "^14.2.0", + "@types/aws-lambda": "^8.10.149", + "@types/node": "^22.14.1", + "cypress": "^14.3.0", "husky": "^9.1.7", - "lint-staged": "^15.5.0", + "lint-staged": "^15.5.1", "prettier": "3.5.3", "start-server-and-test": "^2.0.11", "xo": "^0.60.0" diff --git a/src/api/error/api.ts b/src/api/error/api.ts new file mode 100644 index 0000000..c574d07 --- /dev/null +++ b/src/api/error/api.ts @@ -0,0 +1,25 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { http, HttpResponse } from 'msw'; + +function handler(pathName: string) { + return [ + http.get(`/${pathName}`, ({ request }) => { + const url = new URL(request.url); + + const statusCode = Number.parseInt( + url.searchParams.get('status') ?? '404', + 10, + ); + + const errorMessage = url.searchParams.get('message') ?? 'Not Found'; + + return HttpResponse.json( + { error: statusCode + ': ' + errorMessage }, + { status: statusCode }, + ); + }), + ]; +} + +export default handler;