diff --git a/README.md b/README.md index 2d894e32b..d77c6408d 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,68 @@ json-server -s ./static json-server -s ./static -s ./node_modules ``` +## Middleware + +```sh +json-server --middleware logger.mjs +``` + +```js +// logger.mjs +import chalk from 'chalk'; + +export default (req, _res, next) => { + const currentDate = new Date().toISOString(); + console.log(chalk.green(req.method), chalk.yellow(req.url), chalk.blue(`${currentDate}`)); + + // Check if the request body is already parsed + if (req.body && Object.keys(req.body).length > 0) { + console.log(chalk.magenta('Body:'), req.body); + } else { + // Manually parse the request body if not already parsed + let body = ''; + req.on('data', (chunk) => { + body += chunk.toString(); + }); + req.on('end', () => { + if (body) { + try { + const parsedBody = JSON.parse(body); + console.log(chalk.magenta('Body:'), parsedBody); + } catch (error) { + console.log(chalk.red('Failed to parse body'), error); + } + } + next(); + }); + return; + } + + next(); +}; +``` + +This will output: + +```sh +Index: +http://localhost:3000/ + +Static files: +Serving ./public directory if it exists + +Endpoints: +http://localhost:3000/posts +http://localhost:3000/comments +http://localhost:3000/profile + +PATCH /posts/1 2025-01-03T08:25:13.138Z +Body: { title: 'foo', body: 'bar', userId: 1 } +POST /posts 2025-01-03T08:25:18.661Z +Body: { title: 'foo', body: 'bar', userId: 1 } +GET /posts 2025-01-03T08:25:20.159Z +``` + ## Notable differences with v0.17 - `id` is always a string and will be generated for you if missing diff --git a/middleware/logger.mjs b/middleware/logger.mjs new file mode 100644 index 000000000..a4a7f2210 --- /dev/null +++ b/middleware/logger.mjs @@ -0,0 +1,31 @@ +import chalk from 'chalk'; + +export default (req, _res, next) => { + const currentDate = new Date().toISOString(); + console.log(chalk.green(req.method), chalk.yellow(req.url), chalk.blue(`${currentDate}`)); + + // Check if the request body is already parsed + if (req.body && Object.keys(req.body).length > 0) { + console.log(chalk.magenta('Body:'), req.body); + } else { + // Manually parse the request body if not already parsed + let body = ''; + req.on('data', (chunk) => { + body += chunk.toString(); + }); + req.on('end', () => { + if (body) { + try { + const parsedBody = JSON.parse(body); + console.log(chalk.magenta('Body:'), parsedBody); + } catch (error) { + console.log(chalk.red('Failed to parse body'), error); + } + } + next(); + }); + return; + } + + next(); +}; \ No newline at end of file diff --git a/package.json b/package.json index a0cfb2993..c325f3a4d 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dev": "tsx watch src/bin.ts fixtures/db.json", "build": "rm -rf lib && tsc", "test": "node --import tsx/esm --test src/*.test.ts", + "logger": "tsx watch src/bin.ts fixtures/db.json --middleware=middleware/logger.mjs", "lint": "eslint src", "prepare": "husky", "prepublishOnly": "npm run build" @@ -60,4 +61,4 @@ "sirv": "^2.0.4", "sort-on": "^6.1.0" } -} +} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index b8c5e79e7..5eaaa4d6e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,6 +16,7 @@ const isProduction = process.env['NODE_ENV'] === 'production' export type AppOptions = { logger?: boolean static?: string[] + middleware?: (req: unknown, res: unknown, next: unknown) => void } const eta = new Eta({ @@ -36,6 +37,11 @@ export function createApp(db: Low, options: AppOptions = {}) { ?.map((path) => (isAbsolute(path) ? path : join(process.cwd(), path))) .forEach((dir) => app.use(sirv(dir, { dev: !isProduction }))) + // Use middleware if specified + if (options.middleware) { + app.use(options.middleware) + } + // CORS app .use((req, res, next) => { diff --git a/src/bin.ts b/src/bin.ts index 4633e5e43..d128b73ae 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node import { existsSync, readFileSync, writeFileSync } from 'node:fs' -import { extname } from 'node:path' +import { extname, resolve } from 'node:path' import { parseArgs } from 'node:util' import chalk from 'chalk' @@ -19,11 +19,12 @@ function help() { console.log(`Usage: json-server [options] Options: - -p, --port Port (default: 3000) - -h, --host Host (default: localhost) - -s, --static Static files directory (multiple allowed) - --help Show this message - --version Show version number + -p, --port Port (default: 3000) + -h, --host Host (default: localhost) + -s, --static Static files directory (multiple allowed) + --middleware Middleware file + --help Show this message + --version Show version number `) } @@ -33,6 +34,7 @@ function args(): { port: number host: string static: string[] + middleware: string } { try { const { values, positionals } = parseArgs({ @@ -53,6 +55,10 @@ function args(): { multiple: true, default: [], }, + middleware: { + type: 'string', + default: '', + }, help: { type: 'boolean', }, @@ -97,9 +103,10 @@ function args(): { // App args and options return { file: positionals[0] ?? '', - port: parseInt(values.port as string), - host: values.host as string, - static: values.static as string[], + port: parseInt(values.port), + host: values.host, + static: values.static, + middleware: values.middleware, } } catch (e) { if ((e as NodeJS.ErrnoException).code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') { @@ -112,7 +119,19 @@ function args(): { } } -const { file, port, host, static: staticArr } = args() +// Load middleware +async function loadMiddleware(middlewarePath: string) { + const resolvedPath = resolve(process.cwd(), middlewarePath) + if (existsSync(resolvedPath)) { + const middlewareModule = await import(resolvedPath) + return middlewareModule.default || middlewareModule + } else { + console.error(`Middleware file not found: ${resolvedPath}`) + process.exit(1) + } +} + +const { file, port, host, static: staticArr, middleware } = args() if (!existsSync(file)) { console.log(chalk.red(`File ${file} not found`)) @@ -139,8 +158,19 @@ const observer = new Observer(adapter) const db = new Low(observer, {}) await db.read() +// Load middleware if specified +let middlewareFunction +if (middleware) { + console.log(chalk.gray(`Loading middleware from ${middleware}`)) + middlewareFunction = await loadMiddleware(middleware) +} + // Create app -const app = createApp(db, { logger: false, static: staticArr }) +const app = createApp(db, { + logger: false, + static: staticArr, + middleware: middlewareFunction, +}) function logRoutes(data: Data) { console.log(chalk.bold('Endpoints:')) @@ -157,6 +187,7 @@ function logRoutes(data: Data) { ) .join('\n'), ) + console.log() } const kaomojis = ['♡⸜(˶˃ ᵕ ˂˶)⸝♡', '♡( ◡‿◡ )', '( ˶ˆ ᗜ ˆ˵ )', '(˶ᵔ ᵕ ᵔ˶)'] diff --git a/test.http b/test.http new file mode 100644 index 000000000..78fed90f9 --- /dev/null +++ b/test.http @@ -0,0 +1,22 @@ +### +GET http://localhost:3000/posts + +### +POST http://localhost:3000/posts +Content-Type: application/json + +{ + "title": "foo", + "body": "bar", + "userId": 1 +} + +### +PATCH http://localhost:3000/posts/1 +Content-Type: application/json + +{ + "title": "foo", + "body": "bar", + "userId": 1 +} \ No newline at end of file