-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
HTTP app with routing
- Loading branch information
Showing
30 changed files
with
7,734 additions
and
1,381 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,3 +37,4 @@ jobs: | |
npm run lint | ||
npm run format | ||
npm run typecheck | ||
npm run test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,3 +4,4 @@ | |
npm run format:fix | ||
npm run lint:fix | ||
npm run typecheck | ||
npm run test |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { HttpAdapter } from './http.adapter'; | ||
import { Router } from './router'; | ||
|
||
export class App { | ||
private readonly http = new HttpAdapter(); | ||
private readonly router = new Router(); | ||
|
||
public listen = this.http.listen.bind(this.http); | ||
public close = this.http.close.bind(this.http); | ||
public registerController = this.router.registerController.bind(this.router); | ||
public registerControllers = this.router.registerControllers.bind( | ||
this.router | ||
); | ||
|
||
constructor() { | ||
this.http.setRequestsHandler(this.router.handleRequest.bind(this.router)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { ContentType } from '../enums/content-type.enum'; | ||
import { parseJson } from './parse-json'; | ||
import { parsePlainText } from './parse-plain-text'; | ||
import { parseUrlencoded } from './parse-urlencoded'; | ||
|
||
type ParserFn = ( | ||
rawBody: string | ||
) => | ||
| string | ||
| Record<string, unknown> | ||
| Promise<string | Record<string, unknown>>; | ||
|
||
const parsersMap: Record<ContentType, ParserFn> = { | ||
[ContentType.JSON]: parseJson, | ||
[ContentType.PlainText]: parsePlainText, | ||
[ContentType.Urlencoded]: parseUrlencoded, | ||
}; | ||
|
||
export async function parseBody( | ||
contentType: ContentType, | ||
rawBody: string | ||
): Promise<ReturnType<typeof parsersMap[typeof contentType]>> { | ||
const parser = parsersMap[contentType]; | ||
|
||
if (!parser) throw new Error('Parser not found for ' + contentType); | ||
|
||
return parser(rawBody); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { HttpException } from '../http-exception'; | ||
import HttpStatus from 'http-status'; | ||
|
||
export function parseJson< | ||
TBody extends Record<string, unknown> = Record<string, unknown> | ||
>(rawBody: string): TBody { | ||
try { | ||
return JSON.parse(rawBody); | ||
} catch { | ||
throw new HttpException(HttpStatus.BAD_REQUEST, 'Invalid JSON body'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export function parsePlainText(rawBody: string): string { | ||
return rawBody; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export function parseUrlencoded(rawBody: string) { | ||
return Object.fromEntries(new URLSearchParams(rawBody)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export const IS_CONTROLLER_METADATA = Symbol('is_controller'); | ||
export const CONTROLLER_PREFIX_METADATA = Symbol('controller_prefix'); | ||
|
||
export const IS_ROUTE_METADATA = Symbol('is_route'); | ||
export const ROUTE_METHOD_METADATA = Symbol('route_method'); | ||
export const ROUTE_PATH_METADATA = Symbol('route_path'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { | ||
IS_CONTROLLER_METADATA, | ||
CONTROLLER_PREFIX_METADATA, | ||
} from '../constants'; | ||
import { normalizePath } from '../utils/normalize-path.util'; | ||
|
||
export function Controller(): ClassDecorator; | ||
export function Controller(prefix: string): ClassDecorator; | ||
export function Controller(prefix = '/'): ClassDecorator { | ||
const normalizedPrefix = normalizePath(prefix); | ||
|
||
return (target: object) => { | ||
Reflect.defineMetadata(IS_CONTROLLER_METADATA, true, target); | ||
Reflect.defineMetadata( | ||
CONTROLLER_PREFIX_METADATA, | ||
normalizedPrefix, | ||
target | ||
); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { HttpMethod } from '../enums/http-method.enum'; | ||
import { | ||
IS_ROUTE_METADATA, | ||
ROUTE_METHOD_METADATA, | ||
ROUTE_PATH_METADATA, | ||
} from '../constants'; | ||
|
||
export interface RouteMetadata { | ||
[ROUTE_PATH_METADATA]?: string; | ||
[ROUTE_METHOD_METADATA]?: HttpMethod; | ||
} | ||
|
||
const defaultRouteMetadata: RouteMetadata = { | ||
[ROUTE_METHOD_METADATA]: HttpMethod.Get, | ||
[ROUTE_PATH_METADATA]: '/', | ||
}; | ||
|
||
export function Route( | ||
metadata: RouteMetadata = defaultRouteMetadata | ||
): MethodDecorator { | ||
const pathMetadata = metadata[ROUTE_PATH_METADATA]; | ||
const path = pathMetadata?.length | ||
? pathMetadata | ||
: defaultRouteMetadata[ROUTE_PATH_METADATA]; | ||
const method = | ||
metadata[ROUTE_METHOD_METADATA] || | ||
defaultRouteMetadata[ROUTE_METHOD_METADATA]; | ||
|
||
return ( | ||
target: object, | ||
propertyKey: string | symbol, | ||
descriptor: PropertyDescriptor | ||
) => { | ||
Reflect.defineMetadata(IS_ROUTE_METADATA, true, descriptor.value); | ||
Reflect.defineMetadata(ROUTE_PATH_METADATA, path, descriptor.value); | ||
Reflect.defineMetadata(ROUTE_METHOD_METADATA, method, descriptor.value); | ||
|
||
return descriptor; | ||
}; | ||
} | ||
|
||
const createRouteDecorator = (method: HttpMethod) => { | ||
return (path?: string): MethodDecorator => { | ||
return Route({ | ||
[ROUTE_METHOD_METADATA]: method, | ||
[ROUTE_PATH_METADATA]: path, | ||
}); | ||
}; | ||
}; | ||
|
||
export const Get = createRouteDecorator(HttpMethod.Get); | ||
|
||
export const Post = createRouteDecorator(HttpMethod.Post); | ||
|
||
export const Put = createRouteDecorator(HttpMethod.Put); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export enum ContentType { | ||
PlainText = 'text/plain', | ||
JSON = 'application/json', | ||
Urlencoded = 'application/x-www-form-urlencoded', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export enum HttpMethod { | ||
Get = 'GET', | ||
Post = 'POST', | ||
Put = 'PUT', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export class HttpException extends Error { | ||
constructor(public statusCode: number, public message: string) { | ||
super(message || 'Internal Server Error'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { | ||
createServer, | ||
IncomingMessage, | ||
Server, | ||
ServerResponse, | ||
} from 'node:http'; | ||
|
||
interface RequestsHandler { | ||
(request: IncomingMessage, response: ServerResponse): void | Promise<void>; | ||
} | ||
|
||
export class HttpAdapter { | ||
private readonly httpServer: Server = createServer(); | ||
private requestsHandler?: RequestsHandler; | ||
|
||
constructor() { | ||
this.setupErrorListeners(); | ||
} | ||
|
||
public setRequestsHandler(handler: RequestsHandler) { | ||
if (this.requestsHandler) { | ||
this.httpServer.off('request', this.requestsHandler); | ||
} | ||
|
||
this.requestsHandler = handler.bind(this); | ||
this.httpServer.on('request', this.requestsHandler); | ||
} | ||
|
||
public async listen(port: string | number, hostname?: string): Promise<void> { | ||
return new Promise((resolve) => { | ||
if (hostname) this.httpServer.listen(+port, hostname, resolve); | ||
else this.httpServer.listen(+port, resolve); | ||
}); | ||
} | ||
|
||
public async close(): Promise<void> { | ||
return new Promise((resolve, reject) => { | ||
this.httpServer.close((error) => { | ||
if (error) reject(error); | ||
resolve(); | ||
}); | ||
}); | ||
} | ||
|
||
private setupErrorListeners() { | ||
this.httpServer.on( | ||
'clientError', | ||
(error: NodeJS.ErrnoException, socket) => { | ||
if (error.code === 'ECONNRESET' || !socket.writable) { | ||
return; | ||
} | ||
|
||
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); | ||
} | ||
); | ||
|
||
this.httpServer.on('error', (error: NodeJS.ErrnoException) => { | ||
if (error.code === 'EADDRINUSE') { | ||
console.error('Error: address in use'); | ||
} | ||
|
||
if (error.code === 'EACCES') { | ||
console.error('Error: port in use, please try another one'); | ||
} | ||
|
||
this.close() | ||
.then(() => { | ||
process.exit(1); | ||
}) | ||
.catch(() => { | ||
console.error('Error: cannot close the server'); | ||
process.exit(1); | ||
}); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export interface RequestData< | ||
TParams extends Record<string, unknown> | unknown = Record<string, unknown>, | ||
TBody = unknown | ||
> { | ||
body: TBody; | ||
params: TParams; | ||
} |
Oops, something went wrong.