From 932f32027b3f17005f34572b437a147b51e3badf Mon Sep 17 00:00:00 2001 From: Joao Castro Date: Fri, 5 Jul 2024 12:16:31 -0300 Subject: [PATCH] [feat] - implementing option to serve static file, implementing response function and creating README file --- .gitignore | 3 +- CHANGELOG.md | 6 ++ README.md | 182 +++++++++++++++++++++++++++++++- README.pt-br.md | 184 ++++++++++++++++++++++++++++++++- package.json | 4 + src/Router/router.interface.ts | 19 ++-- src/Router/router.ts | 23 ++++- src/Server/server.interface.ts | 13 +-- src/Server/server.ts | 87 +++++++++++++--- src/typings/global.d.ts | 15 +++ src/typings/router.d.ts | 30 +++++- src/typings/server.d.ts | 20 ++++ src/utils/consts.ts | 16 +++ 13 files changed, 552 insertions(+), 50 deletions(-) create mode 100644 src/typings/server.d.ts create mode 100644 src/utils/consts.ts diff --git a/.gitignore b/.gitignore index 54d75ff..75bde01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules dist -src/dev.ts \ No newline at end of file +src/dev.ts +src/static \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e68e0db..3c7c144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Creating Simpler server README.md file. + +- Creating Simpler server response method. + +- Creating Simpler server methods to serve static files. + - Creating Simpler server base structure (Server, Router and Logger) to deal with API routes. - Adding github actions, changelog and readme files. diff --git a/README.md b/README.md index 7e39625..c2e9921 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,185 @@ ![Node.js](https://img.shields.io/badge/Node.js-v14.17.3-green) ![License](https://img.shields.io/badge/license-MIT-blue) -Simpler is a simple Node.js server, designed to rapidly provide a base to start your server projects. +Simpler is a lightweight, customizable Node.js server framework. It allows you to define routes, handle dynamic path variables, query parameters, and serve static files with ease. + +## Installation + +To install Simpler, you can use npm: + +```bash +npm install simpler +``` + +## Usage + +### Creating an Instance + +You can create an instance of Simpler by importing it and optionally enabling verbose logging: + +```typescript +import Simpler from "simpler"; + +const simpler = new Simpler(true); // Enable verbose logging +``` + +### Defining Routes + +You can define routes using the `addRoute` or the `addRoutes` methods. The same route can handle multiple methods, and you can define dynamic path variables using `:`. The callback function for a route will always receive `req`, `res`, `body`, `pathVariables`, and `queryParams`. + +pathVariables and queryParams are the objects with the values of the path parameters and query parameters. + +Simpler has a response method that will run the default response methods, you can either set a response via simpler or directly in res, below are some examples and their equivalent: + +```typescript +//Returning with simpler +simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + +//Equivalent returning directly with res +res.writeHead(200, { "Content-Type": "application/json" }); +res.end(JSON.stringify(parsedBody)); + +//Returning with simpler +simpler.response(res, 200, "text/html", data); + +//Equivalent returning directly with res +res.writeHead(200, { "Content-Type": "text/html" }); +res.end(data); +``` + +Below you'll find examples on how to use the `addRoute` method. + +```typescript +simpler.router.addRoute( + "/test", + ["POST", "GET"], + (_req, res, body, _pathVariables, _queryParams) => { + const parsedBody = JSON.parse(body); + simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + return; + } +); + +simpler.router.addRoute( + "/test/:id", + ["POST", "GET"], + (_req, res, body, _pathVariables, _queryParams) => { + const parsedBody = JSON.parse(body); + /* + Path variables example: + pathVariables: { + "id": "{value}" + } + */ + simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + return; + } +); + +simpler.router.addRoute( + "/test/:id/:xpto", + ["POST", "GET"], + (_req, res, body, _pathVariables, _queryParams) => { + const parsedBody = JSON.parse(body); + /* + Path variables example: + pathVariables: { + "id": "{value1}", + "xpto": "{value2}", + } + */ + simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + return; + } +); +``` + +### Serving Static Files + +You can configure Simpler to serve static files from a directory using the `addStaticDirectory` or `addStaticDirectories` methods. Additionally, you can load an HTML page in a route. + +```typescript +import path from "path"; +import { readFile } from "fs"; + +simpler.router.addStaticDirectory("static"); + +simpler.router.addRoute("/static", ["GET"], (_req, res) => { + const testePath = path.join(__dirname, "static", "test.html"); + readFile(testePath, (err, data) => { + if (err) { + simpler.response(res, 500, "text/plain", "500 Internal Server Error"); + return; + } + + simpler.response(res, 200, "text/html", data); + }); +}); +``` + +### Starting the Server + +To start the server, use the `listen` method. You can specify the port number, which defaults to 3000 if not provided. + +```typescript +simpler.listen(3001); +``` + +### Implementation Example + +Here is an example of a full setup using Simpler: + +```typescript +import { readFile } from "fs"; +import Simpler from "simpler"; +import path from "path"; + +const simpler = new Simpler(true); + +simpler.router.addRoute("/test", ["POST", "GET"], (_req, res, body) => { + const parsedBody = JSON.parse(body); + simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + return; +}); + +simpler.router.addRoute( + "/test/:id", + ["POST", "GET"], + (_req, res, body: string, _pathVariables, _queryParams) => { + const parsedBody = JSON.parse(body); + simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + return; + } +); + +simpler.router.addRoute( + "/test/:id/:xpto", + ["POST", "GET"], + (_req, res, body, _pathVariables, _queryParams) => { + const parsedBody = JSON.parse(body); + simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + return; + } +); + +simpler.router.addStaticDirectory("static"); + +simpler.router.addRoute("/static", ["GET"], (_req, res) => { + const testePath = path.join(__dirname, "static", "teste.html"); + readFile(testePath, (err, data) => { + if (err) { + simpler.response(res, 500, "text/plain", "500 Internal Server Error"); + return; + } + + simpler.response(res, 200, "text/html", data); + }); +}); + +simpler.listen(3001); +``` + +By following these instructions, you can set up and run your Simpler server, configure routes, handle dynamic path variables, query parameters, and serve static files with ease. ## License @@ -11,4 +189,4 @@ This project is licensed under the MID license. Check file [LICENSE](LICENSE) fo # Other versions -[Readme in Portuguese (PT-BR)](README.pt-br.md) \ No newline at end of file +[Readme in Portuguese (PT-BR)](README.pt-br.md) diff --git a/README.pt-br.md b/README.pt-br.md index 13978f3..04a0415 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -3,12 +3,190 @@ ![Node.js](https://img.shields.io/badge/Node.js-v14.17.3-green) ![License](https://img.shields.io/badge/license-MIT-blue) -Simpler é um servidor Node.js simples, projetado para fornecer uma base para iniciar rapidamente seus projetos de servidor. +Simpler é um framework de servidor Node.js leve e personalizável. Ele permite definir rotas, lidar com variáveis de caminho dinâmicas, parâmetros de consulta e servir arquivos estáticos com facilidade. + +## Instalação + +Para instalar o Simpler, você pode usar o npm: + +```bash +npm install simpler +``` + +## Uso + +### Criando uma Instância + +Você pode criar uma instância do Simpler importando-o e, opcionalmente, habilitando logs verbosos: + +```typescript +import Simpler from "simpler"; + +const simpler = new Simpler(true); // Habilitar logs verbosos +``` + +### Definindo Rotas + +Você pode definir rotas usando os métodos `addRoute` ou `addRoutes`. A mesma rota pode lidar com múltiplos métodos, e você pode definir variáveis de caminho dinâmicas usando `:`. A função de callback para uma rota sempre receberá `req`, `res`, `body`, `pathVariables` e `queryParams`. + +`pathVariables` e `queryParams` são objetos com os valores dos parâmetros de caminho e de consulta. + +Simpler possui um método de resposta que executará os métodos de resposta padrão. Você pode definir uma resposta via Simpler ou diretamente em `res`. Abaixo estão alguns exemplos e seus equivalentes: + +```typescript +// Retornando com Simpler +simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + +// Equivalente retornando diretamente com res +res.writeHead(200, { "Content-Type": "application/json" }); +res.end(JSON.stringify(parsedBody)); + +// Retornando com Simpler +simpler.response(res, 200, "text/html", data); + +// Equivalente retornando diretamente com res +res.writeHead(200, { "Content-Type": "text/html" }); +res.end(data); +``` + +Abaixo, você encontrará exemplos de como usar o método `addRoute`. + +```typescript +simpler.router.addRoute( + "/test", + ["POST", "GET"], + (_req, res, body, _pathVariables, _queryParams) => { + const parsedBody = JSON.parse(body); + simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + return; + } +); + +simpler.router.addRoute( + "/test/:id", + ["POST", "GET"], + (_req, res, body, _pathVariables, _queryParams) => { + const parsedBody = JSON.parse(body); + /* + Exemplo de variáveis de caminho: + pathVariables: { + "id": "{valor}" + } + */ + simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + return; + } +); + +simpler.router.addRoute( + "/test/:id/:xpto", + ["POST", "GET"], + (_req, res, body, _pathVariables, _queryParams) => { + const parsedBody = JSON.parse(body); + /* + Exemplo de variáveis de caminho: + pathVariables: { + "id": "{valor1}", + "xpto": "{valor2}", + } + */ + simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + return; + } +); +``` + +### Servindo Arquivos Estáticos + +Você pode configurar o Simpler para servir arquivos estáticos de um diretório usando os métodos `addStaticDirectory` ou `addStaticDirectories`. Além disso, você pode carregar uma página HTML em uma rota. + +```typescript +import path from "path"; +import { readFile } from "fs"; + +simpler.router.addStaticDirectory("static"); + +simpler.router.addRoute("/static", ["GET"], (_req, res) => { + const testePath = path.join(__dirname, "static", "test.html"); + readFile(testePath, (err, data) => { + if (err) { + simpler.response(res, 500, "text/plain", "500 Internal Server Error"); + return; + } + + simpler.response(res, 200, "text/html", data); + }); +}); +``` + +### Iniciando o Servidor + +Para iniciar o servidor, use o método `listen`. Você pode especificar o número da porta, que por padrão é 3000 se não for fornecido. + +```typescript +simpler.listen(3001); +``` + +### Exemplo de Implementação + +Aqui está um exemplo de uma configuração completa usando Simpler: + +```typescript +import { readFile } from "fs"; +import Simpler from "simpler"; +import path from "path"; + +const simpler = new Simpler(true); + +simpler.router.addRoute("/test", ["POST", "GET"], (_req, res, body) => { + const parsedBody = JSON.parse(body); + simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + return; +}); + +simpler.router.addRoute( + "/test/:id", + ["POST", "GET"], + (_req, res, body: string, _pathVariables, _queryParams) => { + const parsedBody = JSON.parse(body); + simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + return; + } +); + +simpler.router.addRoute( + "/test/:id/:xpto", + ["POST", "GET"], + (_req, res, body, _pathVariables, _queryParams) => { + const parsedBody = JSON.parse(body); + simpler.response(res, 200, "application/json", JSON.stringify(parsedBody)); + return; + } +); + +simpler.router.addStaticDirectory("static"); + +simpler.router.addRoute("/static", ["GET"], (_req, res) => { + const testePath = path.join(__dirname, "static", "teste.html"); + readFile(testePath, (err, data) => { + if (err) { + simpler.response(res, 500, "text/plain", "500 Internal Server Error"); + return; + } + + simpler.response(res, 200, "text/html", data); + }); +}); + +simpler.listen(3001); +``` + +Seguindo estas instruções, você pode configurar e executar seu servidor Simpler, configurar rotas, lidar com variáveis de caminho dinâmicas, parâmetros de consulta e servir arquivos estáticos com facilidade. ## Licença -Este projeto está licenciado sob a Licença MIT. Veja o arquivo [LICENSE](LICENSE) para mais detalhes. +Este projeto é licenciado sob a licença MIT. Consulte o arquivo [LICENSE](LICENSE) para mais detalhes. # Outras versões -[Readme em inglês (EN)](README.md) \ No newline at end of file +[Readme em Inglês (EN)](README.md) \ No newline at end of file diff --git a/package.json b/package.json index 7552cfc..8c57e45 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,10 @@ "lint": "eslint . --ext .ts", "test": "jest" }, + "repository": { + "type": "git", + "url": "git@github.com:JooLuiz/simpler.git" + }, "keywords": [], "author": "João Luiz de Castro (https://github.com/JooLuiz)", "license": "ISC", diff --git a/src/Router/router.interface.ts b/src/Router/router.interface.ts index 68b11c1..f110e90 100644 --- a/src/Router/router.interface.ts +++ b/src/Router/router.interface.ts @@ -1,15 +1,12 @@ interface IRouter { - addRoute: (url: string, method: METHODS[], callback: RouteCallback) => void; - addRoutes: (routes: [Route]) => void; - getRoute: ( - url: string | undefined, - method: string | undefined - ) => Route | null; - getPathVariables: ( - reqUrl: string | undefined, - routeUrl: string - ) => Record; - getqueryParams: (url: string | undefined) => Record; + addRoute: AddRouteFunction; + addRoutes: AddRoutesFunction; + getRoute: GetRouteFunction; + getPathVariables: GetPathVariablesFunction; + getQueryParams: GetQueryParamsFunction; + addStaticDirectory: AddStaticDirectoryFunction; + addStaticDirectories: AddStaticDirectoriesFunction; + getStaticDirs: GetStaticDirsFunction; } export default IRouter; diff --git a/src/Router/router.ts b/src/Router/router.ts index b5dda5e..febbc9d 100644 --- a/src/Router/router.ts +++ b/src/Router/router.ts @@ -1,11 +1,10 @@ import type IRouter from "./router.interface"; class Router implements IRouter { - private routes: Route[]; + private routes: Route[] = []; + private staticDirs: string[] = ["public"]; - constructor() { - this.routes = []; - } + constructor() {} addRoute(url: string, method: METHODS[], callback: RouteCallback): void { this.routes.push({ @@ -67,7 +66,7 @@ class Router implements IRouter { return pathVariables; } - getqueryParams(url: string | undefined): Record { + getQueryParams(url: string | undefined): Record { if (!url) return {}; const queryParams: Record = {}; @@ -83,5 +82,19 @@ class Router implements IRouter { return queryParams; } + + addStaticDirectory(directory: string) { + this.staticDirs.push(directory); + } + + addStaticDirectories(directories: string[]) { + directories.forEach((dir) => { + this.addStaticDirectory(dir); + }); + } + + getStaticDirs() { + return this.staticDirs; + } } export default Router; diff --git a/src/Server/server.interface.ts b/src/Server/server.interface.ts index 2fa4468..d497941 100644 --- a/src/Server/server.interface.ts +++ b/src/Server/server.interface.ts @@ -1,14 +1,7 @@ -import { IncomingMessage, ServerResponse } from "http"; - interface IServer { - handleRequest: ( - req: IncomingMessage, - res: ServerResponse, - route: Route, - pathVariables: Record, - queryParams: Record - ) => void; - listen: (port: number) => void; + handleRequest: HandleRequestFunction; + listen: ListenFunction; + response: ResponseFunction; } export default IServer; diff --git a/src/Server/server.ts b/src/Server/server.ts index 3c7962a..918cf3a 100644 --- a/src/Server/server.ts +++ b/src/Server/server.ts @@ -4,6 +4,9 @@ import type IRouter from "../Router/router.interface"; import type ILogger from "../Logger/logger.interface"; import Router from "../Router"; import Logger from "../Logger"; +import path from "path"; +import { fileTypes } from "../utils/consts"; +import { readFile } from "fs/promises"; class Server implements IServer { private httpServer: http.Server | undefined; @@ -42,12 +45,7 @@ class Server implements IServer { (err as Error).message }` ); - res.writeHead(500, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - message: (err as Error).message || "Something went wrong", - }) - ); + this.errorResponse(res, err as Error); } }); } @@ -56,14 +54,22 @@ class Server implements IServer { const srvr = http.createServer( async (req: IncomingMessage, res: ServerResponse) => { try { + const isStaticPage = await this.serveStaticFiles(req, res); + if (isStaticPage) { + return; + } const route = this.router.getRoute(req.url, req.method); if (!route) { this.logger.logIfVerbose( `Route ${req.url} not Found`, this.isVerbose ); - res.writeHead(404, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ message: "Route Not Found" })); + this.response( + res, + 404, + "application/json", + JSON.stringify({ message: "Route Not Found" }) + ); return; } @@ -77,7 +83,7 @@ class Server implements IServer { this.isVerbose ); - const queryParams = this.router.getqueryParams(req.url); + const queryParams = this.router.getQueryParams(req.url); this.logger.logIfVerbose( `Query Params: ${JSON.stringify(queryParams)}`, @@ -92,13 +98,7 @@ class Server implements IServer { }` ); - res.writeHead(500, { "Content-Type": "application/json" }); - - res.end( - JSON.stringify({ - message: (err as Error).message || "Something went wrong", - }) - ); + this.errorResponse(res, err as Error); } } ); @@ -106,7 +106,7 @@ class Server implements IServer { this.httpServer = srvr; } - listen(port: number) { + listen(port?: number) { if (!this.httpServer) { this.createServer(); } @@ -121,6 +121,59 @@ class Server implements IServer { }); } } + + async serveStaticFiles( + req: IncomingMessage, + res: ServerResponse + ): Promise { + if (!req.url) { + return false; + } + + const staticDirs = this.router.getStaticDirs(); + for (const dir of staticDirs) { + const publicDirectory = path.join(__dirname, `../${dir}`); + const filePath = path.join( + publicDirectory, + req.url === "/" ? "index.html" : req.url || "" + ); + this.logger.logIfVerbose(`Static File Path: ${filePath}`, this.isVerbose); + const extname = String(path.extname(filePath)).toLowerCase(); + const contentType = + fileTypes[extname as FILE_EXTENSIONS] || "application/octet-stream"; + try { + const content = await readFile(filePath); + this.response(res, 200, contentType, content); + return true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + this.logger.logIfVerbose((error as Error).message, this.isVerbose); + } + } + } + return false; + } + + response( + res: ServerResponse, + status: number, + contentType: string, + message: T + ) { + res.writeHead(status, { "Content-Type": contentType }); + res.end(message); + } + + private errorResponse(res: ServerResponse, err: Error) { + this.response( + res, + 500, + "application/json", + JSON.stringify({ + message: (err as Error).message || "Something went wrong", + }) + ); + } } export default Server; diff --git a/src/typings/global.d.ts b/src/typings/global.d.ts index e69de29..30a30a3 100644 --- a/src/typings/global.d.ts +++ b/src/typings/global.d.ts @@ -0,0 +1,15 @@ +type FILE_EXTENSIONS = + | ".html" + | ".js" + | ".css" + | ".json" + | ".png" + | ".jpg" + | ".gif" + | ".wav" + | ".mp4" + | ".woff" + | ".ttf" + | ".eot" + | ".otf" + | ".svg"; diff --git a/src/typings/router.d.ts b/src/typings/router.d.ts index 015f22b..0b45324 100644 --- a/src/typings/router.d.ts +++ b/src/typings/router.d.ts @@ -10,10 +10,38 @@ declare global { type RouteCallback = ( req: IncomingMessage, res: ServerResponse, - body: T, + body: string, pathVariables: Record, queryParams: Record ) => void; type METHODS = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + + type AddRouteFunction = ( + url: string, + method: METHODS[], + callback: RouteCallback + ) => void; + + type AddRoutesFunction = (routes: [Route]) => void; + + type GetRouteFunction = ( + url: string | undefined, + method: string | undefined + ) => Route | null; + + type GetPathVariablesFunction = ( + reqUrl: string | undefined, + routeUrl: string + ) => Record; + + type GetQueryParamsFunction = ( + url: string | undefined + ) => Record; + + type AddStaticDirectoryFunction = (directory: string) => void; + + type AddStaticDirectoriesFunction = (directories: string[]) => void; + + type GetStaticDirsFunction = () => string[]; } diff --git a/src/typings/server.d.ts b/src/typings/server.d.ts new file mode 100644 index 0000000..a872dd1 --- /dev/null +++ b/src/typings/server.d.ts @@ -0,0 +1,20 @@ +import { IncomingMessage, ServerResponse } from "http"; + +declare global { + type HandleRequestFunction = ( + req: IncomingMessage, + res: ServerResponse, + route: Route, + pathVariables: Record, + queryParams: Record + ) => void; + + type ListenFunction = (port?: number) => void; + + type ResponseFunction = ( + res: ServerResponse, + status: number, + contentType: string, + message: T + ) => void; +} diff --git a/src/utils/consts.ts b/src/utils/consts.ts new file mode 100644 index 0000000..990ff1c --- /dev/null +++ b/src/utils/consts.ts @@ -0,0 +1,16 @@ +export const fileTypes: Record = { + '.html': 'text/html', + '.js': 'application/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpg', + '.gif': 'image/gif', + '.wav': 'audio/wav', + '.mp4': 'video/mp4', + '.woff': 'application/font-woff', + '.ttf': 'application/font-ttf', + '.eot': 'application/vnd.ms-fontobject', + '.otf': 'application/font-otf', + '.svg': 'application/image/svg+xml', +}; \ No newline at end of file