Skip to content
Open
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
24 changes: 9 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ However it's not correct, because you don't know that before I registered this h
```js
app.use((req, res) => {
res.sendStatus(400);
})
});
```

So to avoid confusion like this, there is no middleware support in HNDL.
Expand All @@ -48,7 +48,7 @@ In HNDL any object that looks like this is a valid endpoint:
type Endpoint<T> = {
accept: (request: Request) => Optional<T> | Promise<Optional<T>>;
handle: (payload: T) => Response | Promise<Response>;
}
};
```

The most important thing to notice is the relationship between `accept` and `handle`:
Expand All @@ -62,11 +62,7 @@ A router in HNDL is defined using the function `router`, it takes a variadic lis
endpoints like this:

```ts
const myRouter = router(
firstEndpoint,
secondEndpoint,
thirdEndpoint
)
const myRouter = router(firstEndpoint, secondEndpoint, thirdEndpoint);
```

The main job of the router is to choose one of the passed endpoints to handle the
Expand Down Expand Up @@ -95,8 +91,10 @@ This endpoint will respond to any request with a 200 OK:
```ts
const everythingIsOK = {
accept: () => true,
handle: () => { status: OK }
}
handle: () => {
status: HttpStatusCode.OK;
},
};
```

This router will respond with `200 OK` if the URL starts with `/ok`,
Expand All @@ -106,21 +104,17 @@ otherwise with `404 Not Found`:
const myRouter = router(
{
accept: request => request.url.startsWith("/ok"),
handle: () => { status: OK }
handle: () => { status: HttpStatusCode.OK }
},
{
accept: () => true,
handle: () { status: NOT_FOUND }
handle: () { status: HttpStatusCode.NOT_FOUND }
}
)
```



## The Service

When developing a web service it's necessary to perform additional
tasks such as logging, error handling, etc. These do not fall
into the area of responsibility of endpoints, and for this we use the `service`.


6 changes: 1 addition & 5 deletions src/example/hello-world.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { createServer } from "http";
import { acceptPath } from "../accept-path";
import { endpoint } from "../endpoint";
import { listener } from "../listener";
import { json } from "../response";
import { service } from "../service";
import { acceptPath, endpoint, json, listener, service } from "..";

const port = process.env.PORT || "3000";

Expand Down
10 changes: 7 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export { listener } from "./listener";
export { endpoint, router } from "./endpoint";
export { service } from "./service";
export * from './accept-path';
export * from './endpoint';
export * from './types';
export * from './status';
export * from './service';
export * from './listener';
export * from './response'
4 changes: 2 additions & 2 deletions src/listener.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IncomingMessage, ServerResponse } from "http";
import { INTERNAL_SERVER_ERROR } from "./status";
import { HttpStatusCode } from "./status";
import { Endpoint, Request, Response } from "./types";

export const listener = <T>(def: Endpoint<T>) => {
Expand All @@ -15,7 +15,7 @@ const acceptAndHandle = async <T>(def: Endpoint<T>, req: Request) => {
}

const listenerErrorResponse: Response = {
status: INTERNAL_SERVER_ERROR,
status: HttpStatusCode.INTERNAL_SERVER_ERROR,
body: "Endpoint did not produce a response",
headers: { "Content-Type": "text/plain" }
}
Expand Down
4 changes: 2 additions & 2 deletions src/response.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { OK } from "./status";
import { HttpStatusCode } from "./status";
import { Response } from "./types";

export const json = (data: any): Response => {
return {
status: OK,
status: HttpStatusCode.OK,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
}
Expand Down
6 changes: 3 additions & 3 deletions src/service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { endpoint, router } from "./endpoint";
import { INTERNAL_SERVER_ERROR, NOT_FOUND } from "./status";
import { HttpStatusCode } from "./status";
import { Endpoint, ErrorHandler, Logger, Request, Response } from "./types";

type ServiceProps = {
Expand Down Expand Up @@ -35,7 +35,7 @@ const produceResponse = async (request: Request, endpoint: Endpoint<any>, errorH

const catchAllEndpoint = endpoint({
accept: () => true,
handle: () => ({ status: NOT_FOUND })
handle: () => ({ status: HttpStatusCode.NOT_FOUND })
});


Expand All @@ -53,5 +53,5 @@ const defaultErrorHandler = (error: any): Response => {

const getErrorStatus = (error: any) => {
if (typeof error?.status?.code === "number" && typeof error?.status?.phrase === "string") return error.status;
return INTERNAL_SERVER_ERROR;
return HttpStatusCode.INTERNAL_SERVER_ERROR;
}
114 changes: 58 additions & 56 deletions src/status.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,60 @@
import { HttpStatus } from "./types";

export const CONTINUE: HttpStatus = { code: 100, phrase: "Continue" };
export const SWITCHING_PROTOCOLS: HttpStatus = { code: 101, phrase: "Switching Protocols" };
export const PROCESSING: HttpStatus = { code: 102, phrase: "Processin" };
export const OK: HttpStatus = { code: 200, phrase: "OK" };
export const CREATED: HttpStatus = { code: 201, phrase: "Created" };
export const ACCEPTED: HttpStatus = { code: 202, phrase: "Accepted" };
export const NON_AUTHORITATIVE_INFORMATION: HttpStatus = { code: 203, phrase: "Non Authoritative Information" };
export const NO_CONTENT: HttpStatus = { code: 204, phrase: "No Content" };
export const RESET_CONTENT: HttpStatus = { code: 205, phrase: "Reset Content" };
export const PARTIAL_CONTENT: HttpStatus = { code: 206, phrase: "Partial Content" };
export const MULTI_STATUS: HttpStatus = { code: 207, phrase: "Multi-Status" };
export const MULTIPLE_CHOICES: HttpStatus = { code: 300, phrase: "Multiple Choices" };
export const MOVED_PERMANENTLY: HttpStatus = { code: 301, phrase: "Moved Permanently" };
export const MOVED_TEMPORARILY: HttpStatus = { code: 302, phrase: "Moved Temporarily" };
export const SEE_OTHER: HttpStatus = { code: 303, phrase: "See Other" };
export const NOT_MODIFIED: HttpStatus = { code: 304, phrase: "Not Modified" };
export const USE_PROXY: HttpStatus = { code: 305, phrase: "Use Proxy" };
export const TEMPORARY_REDIRECT: HttpStatus = { code: 307, phrase: "Temporary Redirect" };
export const PERMANENT_REDIRECT: HttpStatus = { code: 308, phrase: "Permanent Redirect" };
export const BAD_REQUEST: HttpStatus = { code: 400, phrase: "Bad Request" };
export const UNAUTHORIZED: HttpStatus = { code: 401, phrase: "Unauthorized" };
export const PAYMENT_REQUIRED: HttpStatus = { code: 402, phrase: "Payment Required" };
export const FORBIDDEN: HttpStatus = { code: 403, phrase: "Forbidden" };
export const NOT_FOUND: HttpStatus = { code: 404, phrase: "Not Found" };
export const METHOD_NOT_ALLOWED: HttpStatus = { code: 405, phrase: "Method Not Allowed" };
export const NOT_ACCEPTABLE: HttpStatus = { code: 406, phrase: "Not Acceptable" };
export const PROXY_AUTHENTICATION_REQUIRED: HttpStatus = { code: 407, phrase: "Proxy Authentication Required" };
export const REQUEST_TIMEOUT: HttpStatus = { code: 408, phrase: "Request Timeout" };
export const CONFLICT: HttpStatus = { code: 409, phrase: "Conflict" };
export const GONE: HttpStatus = { code: 410, phrase: "Gone" };
export const LENGTH_REQUIRED: HttpStatus = { code: 411, phrase: "Length Required" };
export const PRECONDITION_FAILED: HttpStatus = { code: 412, phrase: "Precondition Failed" };
export const REQUEST_TOO_LONG: HttpStatus = { code: 413, phrase: "Request Entity Too Large" };
export const REQUEST_URI_TOO_LONG: HttpStatus = { code: 414, phrase: "Request-URI Too Long" };
export const UNSUPPORTED_MEDIA_TYPE: HttpStatus = { code: 415, phrase: "Unsupported Media Type" };
export const REQUESTED_RANGE_NOT_SATISFIABLE: HttpStatus = { code: 416, phrase: "Requested Range Not Satisfiable" };
export const EXPECTATION_FAILED: HttpStatus = { code: 417, phrase: "Expectation Failed" };
export const IM_A_TEAPOT: HttpStatus = { code: 418, phrase: "I'm a teapot" };
export const INSUFFICIENT_SPACE_ON_RESOURCE: HttpStatus = { code: 419, phrase: "Insufficient Space on Resource" };
export const METHOD_FAILURE: HttpStatus = { code: 420, phrase: "Method Failure" };
export const MISDIRECTED_REQUEST: HttpStatus = { code: 421, phrase: "Misdirected Request" };
export const UNPROCESSABLE_ENTITY: HttpStatus = { code: 422, phrase: "Unprocessable Entity" };
export const LOCKED: HttpStatus = { code: 423, phrase: "Locked" };
export const FAILED_DEPENDENCY: HttpStatus = { code: 424, phrase: "Failed Dependency" };
export const PRECONDITION_REQUIRED: HttpStatus = { code: 428, phrase: "Precondition Required" };
export const TOO_MANY_REQUESTS: HttpStatus = { code: 429, phrase: "Too Many Requests" };
export const REQUEST_HEADER_FIELDS_TOO_LARGE: HttpStatus = { code: 431, phrase: "Request Header Fields Too Large" };
export const UNAVAILABLE_FOR_LEGAL_REASONS: HttpStatus = { code: 451, phrase: "Unavailable For Legal Reasons" };
export const INTERNAL_SERVER_ERROR: HttpStatus = { code: 500, phrase: "Internal Server Error" };
export const NOT_IMPLEMENTED: HttpStatus = { code: 501, phrase: "Not Implemented" };
export const BAD_GATEWAY: HttpStatus = { code: 502, phrase: "Bad Gateway" };
export const SERVICE_UNAVAILABLE: HttpStatus = { code: 503, phrase: "Service Unavailable" };
export const GATEWAY_TIMEOUT: HttpStatus = { code: 504, phrase: "Gateway Timeout" };
export const HTTP_VERSION_NOT_SUPPORTED: HttpStatus = { code: 505, phrase: "HTTP Version Not Supported" };
export const INSUFFICIENT_STORAGE: HttpStatus = { code: 507, phrase: "Insufficient Storage" };
export const NETWORK_AUTHENTICATION_REQUIRED: HttpStatus = { code: 511, phrase: "Network Authentication Required" };
export abstract class HttpStatusCode {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, what's the benefit of having a class vs a simple module?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the class format could be better for encapsulation and organization.

public static CONTINUE: HttpStatus = { code: 100, phrase: "Continue" };
public static SWITCHING_PROTOCOLS: HttpStatus = { code: 101, phrase: "Switching Protocols" };
public static PROCESSING: HttpStatus = { code: 102, phrase: "Processin" };
public static OK: HttpStatus = { code: 200, phrase: "OK" };
public static CREATED: HttpStatus = { code: 201, phrase: "Created" };
public static ACCEPTED: HttpStatus = { code: 202, phrase: "Accepted" };
public static NON_AUTHORITATIVE_INFORMATION: HttpStatus = { code: 203, phrase: "Non Authoritative Information" };
public static NO_CONTENT: HttpStatus = { code: 204, phrase: "No Content" };
public static RESET_CONTENT: HttpStatus = { code: 205, phrase: "Reset Content" };
public static PARTIAL_CONTENT: HttpStatus = { code: 206, phrase: "Partial Content" };
public static MULTI_STATUS: HttpStatus = { code: 207, phrase: "Multi-Status" };
public static MULTIPLE_CHOICES: HttpStatus = { code: 300, phrase: "Multiple Choices" };
public static MOVED_PERMANENTLY: HttpStatus = { code: 301, phrase: "Moved Permanently" };
public static MOVED_TEMPORARILY: HttpStatus = { code: 302, phrase: "Moved Temporarily" };
public static SEE_OTHER: HttpStatus = { code: 303, phrase: "See Other" };
public static NOT_MODIFIED: HttpStatus = { code: 304, phrase: "Not Modified" };
public static USE_PROXY: HttpStatus = { code: 305, phrase: "Use Proxy" };
public static TEMPORARY_REDIRECT: HttpStatus = { code: 307, phrase: "Temporary Redirect" };
public static PERMANENT_REDIRECT: HttpStatus = { code: 308, phrase: "Permanent Redirect" };
public static BAD_REQUEST: HttpStatus = { code: 400, phrase: "Bad Request" };
public static UNAUTHORIZED: HttpStatus = { code: 401, phrase: "Unauthorized" };
public static PAYMENT_REQUIRED: HttpStatus = { code: 402, phrase: "Payment Required" };
public static FORBIDDEN: HttpStatus = { code: 403, phrase: "Forbidden" };
public static NOT_FOUND: HttpStatus = { code: 404, phrase: "Not Found" };
public static METHOD_NOT_ALLOWED: HttpStatus = { code: 405, phrase: "Method Not Allowed" };
public static NOT_ACCEPTABLE: HttpStatus = { code: 406, phrase: "Not Acceptable" };
public static PROXY_AUTHENTICATION_REQUIRED: HttpStatus = { code: 407, phrase: "Proxy Authentication Required" };
public static REQUEST_TIMEOUT: HttpStatus = { code: 408, phrase: "Request Timeout" };
public static CONFLICT: HttpStatus = { code: 409, phrase: "Conflict" };
public static GONE: HttpStatus = { code: 410, phrase: "Gone" };
public static LENGTH_REQUIRED: HttpStatus = { code: 411, phrase: "Length Required" };
public static PRECONDITION_FAILED: HttpStatus = { code: 412, phrase: "Precondition Failed" };
public static REQUEST_TOO_LONG: HttpStatus = { code: 413, phrase: "Request Entity Too Large" };
public static REQUEST_URI_TOO_LONG: HttpStatus = { code: 414, phrase: "Request-URI Too Long" };
public static UNSUPPORTED_MEDIA_TYPE: HttpStatus = { code: 415, phrase: "Unsupported Media Type" };
public static REQUESTED_RANGE_NOT_SATISFIABLE: HttpStatus = { code: 416, phrase: "Requested Range Not Satisfiable" };
public static EXPECTATION_FAILED: HttpStatus = { code: 417, phrase: "Expectation Failed" };
public static IM_A_TEAPOT: HttpStatus = { code: 418, phrase: "I'm a teapot" };
public static INSUFFICIENT_SPACE_ON_RESOURCE: HttpStatus = { code: 419, phrase: "Insufficient Space on Resource" };
public static METHOD_FAILURE: HttpStatus = { code: 420, phrase: "Method Failure" };
public static MISDIRECTED_REQUEST: HttpStatus = { code: 421, phrase: "Misdirected Request" };
public static UNPROCESSABLE_ENTITY: HttpStatus = { code: 422, phrase: "Unprocessable Entity" };
public static LOCKED: HttpStatus = { code: 423, phrase: "Locked" };
public static FAILED_DEPENDENCY: HttpStatus = { code: 424, phrase: "Failed Dependency" };
public static PRECONDITION_REQUIRED: HttpStatus = { code: 428, phrase: "Precondition Required" };
public static TOO_MANY_REQUESTS: HttpStatus = { code: 429, phrase: "Too Many Requests" };
public static REQUEST_HEADER_FIELDS_TOO_LARGE: HttpStatus = { code: 431, phrase: "Request Header Fields Too Large" };
public static UNAVAILABLE_FOR_LEGAL_REASONS: HttpStatus = { code: 451, phrase: "Unavailable For Legal Reasons" };
public static INTERNAL_SERVER_ERROR: HttpStatus = { code: 500, phrase: "Internal Server Error" };
public static NOT_IMPLEMENTED: HttpStatus = { code: 501, phrase: "Not Implemented" };
public static BAD_GATEWAY: HttpStatus = { code: 502, phrase: "Bad Gateway" };
public static SERVICE_UNAVAILABLE: HttpStatus = { code: 503, phrase: "Service Unavailable" };
public static GATEWAY_TIMEOUT: HttpStatus = { code: 504, phrase: "Gateway Timeout" };
public static HTTP_VERSION_NOT_SUPPORTED: HttpStatus = { code: 505, phrase: "HTTP Version Not Supported" };
public static INSUFFICIENT_STORAGE: HttpStatus = { code: 507, phrase: "Insufficient Storage" };
public static NETWORK_AUTHENTICATION_REQUIRED: HttpStatus = { code: 511, phrase: "Network Authentication Required" };
}
8 changes: 4 additions & 4 deletions tests/listener.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { deepStrictEqual } from "assert";
import { listener } from "../src/listener";
import { INTERNAL_SERVER_ERROR, OK } from "../src/status";
import { HttpStatusCode } from "../src/status";
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the purpose of having the module is just to have a namespace for status codes, you can always import like this, without having to add a custom class.

import * as HttpStatusCode from "../src/status"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely correct, and the only reasons I designed it this way were for potential future improvements/updates and to lessen the number of imports when using more than one status code in a single file, and because the class structure might allow for greater encapsulation and organization.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate your effort here, and I understand your point about reducing the number of potential imports / exports in a file. Can this be solved in the same way that you cleaned up the exports in the index.ts?

Something like this maybe?

export * as HttpStatusCode from "./status";

I'm just cautious about using classes as they come with some more baggage, that in this particular case we can probably get away without.

What do you think?

Copy link
Author

@muhayli muhayli Feb 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, you're right, with this type of export/import
export * as HttpStatusCode from "./status";
The class doesn't seem to be necessary. since reducing import/export was my primary motivation for taking the class, and this appears to have fixed it.

import { Response } from "../src/types";

export const listenerTests = async () => {
{
const response: Response = {
status: OK,
status: HttpStatusCode.OK,
headers: { "Content-Type": "text/plain" },
body: "Hello World"
}
Expand Down Expand Up @@ -39,7 +39,7 @@ export const listenerTests = async () => {
{
const testListener = listener({
accept: () => false,
handle: () => ({ status: OK })
handle: () => ({ status: HttpStatusCode.OK })
});

const actual = {
Expand All @@ -63,7 +63,7 @@ export const listenerTests = async () => {
deepStrictEqual(
actual,
{
status: INTERNAL_SERVER_ERROR,
status: HttpStatusCode.INTERNAL_SERVER_ERROR,
body: "Endpoint did not produce a response",
headers: { "Content-Type": "text/plain" }
},
Expand Down
4 changes: 2 additions & 2 deletions tests/response.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { deepStrictEqual } from "assert";
import { json } from "../src/response";
import { OK } from "../src/status";
import { HttpStatusCode } from "../src/status";

export const responseTests = () => {

Expand All @@ -10,7 +10,7 @@ export const responseTests = () => {
deepStrictEqual(
response,
{
status: OK,
status: HttpStatusCode.OK,
headers: { "Content-Type": "application/json" },
body: '{"hello":"World"}'
}
Expand Down
18 changes: 9 additions & 9 deletions tests/router.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { deepStrictEqual } from "assert";
import { router } from "../src/endpoint";
import { BAD_REQUEST, MOVED_TEMPORARILY, OK } from "../src/status";
import { HttpStatusCode } from "../src/status";

export const routerTests = async () => {
{
const testRouter = router(
{
accept: () => false,
handle: () => ({ status: OK })
handle: () => ({ status: HttpStatusCode.OK })
},
{
accept: () => true,
handle: () => ({ status: BAD_REQUEST })
handle: () => ({ status: HttpStatusCode.BAD_REQUEST })
},
{
accept: () => true,
handle: () => ({ status: MOVED_TEMPORARILY })
handle: () => ({ status: HttpStatusCode.MOVED_TEMPORARILY })
}
)

const response = await testRouter.handle(await testRouter.accept({} as any));
deepStrictEqual(response.status, BAD_REQUEST, "Router should choose the first endpoint that accepts");
deepStrictEqual(response.status, HttpStatusCode.BAD_REQUEST, "Router should choose the first endpoint that accepts");
}

{
Expand All @@ -30,24 +30,24 @@ export const routerTests = async () => {
const testRouter = router(
{
accept: () => false,
handle: () => ({ status: MOVED_TEMPORARILY })
handle: () => ({ status: HttpStatusCode.MOVED_TEMPORARILY })
},
{
accept: () => truthyPayload,
handle: payload => {
handledPayload = payload;
return { status: OK }
return { status: HttpStatusCode.OK }
}
},
{
accept: () => true,
handle: () => ({ status: BAD_REQUEST })
handle: () => ({ status: HttpStatusCode.BAD_REQUEST })
}
)

const response = await testRouter.handle(await testRouter.accept({} as any));

deepStrictEqual(response.status, OK, "Router should accept any truthy value");
deepStrictEqual(response.status, HttpStatusCode.OK, "Router should accept any truthy value");
deepStrictEqual(handledPayload, truthyPayload, "Router should pass the correct payload to handle")
}
}
Loading