Resources
What Is a Resource?
Just like the MDN's documentation pages on identifying resources, a resource in Drash is the target of an HTTP request. In Drash, resources are represented using classes with pathnames (we call these paths) and HTTP request methods. Resource classes are written like:
class MyResource {
public paths = [ // These are the pathnames (we use `paths` for short)
"/teas",
];
public GET(...) {...} // If a client makes a GET /teas request, Drash will
// route that request to this `GET(...)` method
public POST(...) {...} // If a client makes a POST /teas request, Drash will
// route that request to this `POST(...)` method
public PUT(...) {...} // If a client makes a PUT /teas request, Drash will
// route that request to this `PUT(...)` method
// ... and other HTTP request methods (code is shortened for brevity)
}
Why Use Classes?
TLDR
We (the maintainers) prefer to use classes when processing resources internally.
Detailed Explanation
Drash does not use an Express-like syntax (e.g., app.get("/teas", someCallback)
) and does not use the term "controllers" like in the MVC pattern to handle requests. Instead, Drash uses classes and uses the term "resources" to handle requests. The initial reason we went with resources and a class syntax is explained in our article: Why We Built Drash. Since publishing that article, we have made a few changes (removing this.request
and this.response
being one of those changes) based on developer experience feedback and a goal of creating a maintainable framework that scales across JavaScript runtimes.
To give you a better idea of how Drash compares to a framework that uses app.get()
, see the JavaScript code blocks below showing how GET requests can be handled in Express and Drash's Request Chain.
Express
This code is based on Express v4.x.
const express = require("express");
const app = express();
const port = 1447;
app.get("/", (req, res) => {
res.send(req.method + " received!");
});
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
})
Drash Request Chain using a context object
The example below shows a resource from the Request Chain module using a "context" object like in Koa.
import { Chain, Resource } from "http://esm.sh/drash/modules/chains/RequestChain/mod.native.ts";
import { serve } from "https://deno.land/std@.../http/server.ts";
const port = 1447;
// Create a resource
class MyResource extends Resource {
public paths = ["/"];
public GET(context) {
context.res = new Response(context.req.method + " received!");
}
}
// Create the chain
const chain = Chain
.builder()
.resources(MyResource)
.build();
// Plug the chain into Deno
const handler = (req) => {
const context = {
url: req.url,
method: req.method,
req,
};
return chain
.handle(context)
.then(() => context.res);
};
console.log(`Server listening at http://localhost:${port}`);
await serve(handler, { port });
Drash Request Chain using Web APIs
The example below shows a resource from the Request Chain module using Web APIs (Request
and Response
).
import { Chain, Resource } from "http://esm.sh/drash/modules/chains/RequestChain/mod.native.ts";
import { serve } from "https://deno.land/std@.../http/server.ts";
const port = 1447;
// Create a resource
class MyResource extends Resource {
public paths = ["/"];
public GET(request) {
return new Response(request.method + " received!");
}
}
// Create the chain
const chain = Chain
.builder()
.resources(MyResource)
.build();
// Plug the chain into Deno
const handler = (request) => {
return chain
.handle(request);
};
console.log(`Server listening at http://localhost:${port}`);
await serve(handler, { port });
Very Strict Interface
You might notice (now or when you start working with resource classes) that the syntax is very strict. This is intentional. We made resource classes follow a strict interface in hopes that it promotes separation of concerns.
For example, if you want to create a resource class to handle users, then we hope your thought process will be:
- your resource should be named
Users
; - its
paths
should contain/users
; - the logic contained in its HTTP methods should be user-related; and
- it should be the only resource in a file named
Users.ts
.
The above thought process is something we beleive will help you separate concerns when building with Drash. In other frameworks, the flexibility of being unopinionated could lead to files having multiple endpoints that do not align with the files' names. This is one reason we decided to add structure (aka us being more opinionated) to Drash resources.
The Core Resource
TLDR
All resources extend the Resource
class located in Drash's core code. Extending this class allows all resources to share the same interface and default behavior — throwing a 501 Not Implemented
error for any HTTP request method not implemented. The decision to throw a 501 Not Implemented
error is based on RFC 7231 Section 4.1.
Detailed Explanation
Drash's core code contains the core resource class: Resource
. Its only purpose is to provide a base resource class that the Standard and Modules codebases can use. This ensures all resources share a single source of default behavior.
The core Resource
class has implementations for all of the HTTP request methods. Those implementations throw a 501 Not Implemented
error. Throw a 501 Not Implemented
error is based on RFC 7231 Section 4.1. The implementations are written like:
class Resource {
public paths: string[] = [];
public GET(_request: unknown): unknown {
throw new HTTPError(Status.NotImplemented);
}
public POST(_request: unknown): unknown {
throw new HTTPError(Status.NotImplemented);
}
public PUT(_request: unknown): unknown {
throw new HTTPError(Status.NotImplemented);
}
// ... and so on for the other HTTP request methods (code shortened for brevity)
}
Given the above code, if we have a MyResource
class that extends Resource
like ...
// Import path is shortened for brevity
import { Resource } from ".../path/to/core/http/Resource.ts";
class MyResource extends Resource {
public paths = ["/my-resource"];
public GET(request: Request): Response {
return new Response(`Your ${request.method} request was received!`);
}
}
... then any request to MyResource
that is not a GET request will result in a 501 Not Implemented
error being thrown.
Throwing a 501 Not Implemented
error is the default behavior for all resources (unless you create your own base resource class that implements a different default behavior).
Chain Resource Classes
TLDR
All chain modules (in the Modules part of the codebase) export a Resource
class. You must extend this class to create your resources. Otherwise, your application might not work as expected.
Detailed Explanation
All chains implement their own base Resource
class. This class extends the core Resource
class. We (the maintainers) do it this way because it allows us:
- create chains that can handle varying data types;
- isolate request-resource-response lifecycle behaviors to specific chains (promoting separation of concerns);
- create new chains to support future JavaScript runtimes and their HTTP servers; and
- keep the default behavior intact.
When you use a chain, you will extend its Resource
class to create your resources. For example, the code below shows you how you would extend the base Resource
class from the Request Chain module:
// Import path is shortened for brevity
import { Resource } from "../path/to/modules/chains/RequestChain/mod.native.ts";
class MyResource extends RequestChain.Resource {
public paths = ["/teas"];
public GET(request: Request): Response {
// ... do something (code is shortened for brevity)
}
}
HTTP request methods
Adding HTTP request methods
Giving your resource classes the ability to handle requests with different HTTP request methods (e.g., GET, POST, PUT, etc.) is done by adding the HTTP request methods in your resource classes. For example:
- If you want to handle GET requests, then define a
GET()
method in your resources - If you want to handle POST requests, then define a
POST()
method in your resources - If you want to handle PUT requests, then define a
PUT()
method in your resources - ... and so on for the other HTTP request methods
You can define the following HTTP request methods in your resources:
- CONNECT (opens in a new tab)
- DELETE (opens in a new tab)
- GET (opens in a new tab)
- HEAD (opens in a new tab)
- OPTIONS (opens in a new tab)
- PATCH (opens in a new tab)
- POST (opens in a new tab)
- PUT (opens in a new tab)
- TRACE (opens in a new tab)
Synchronous/Asynchronous
Your resources' HTTP request methods can be synchronous or asynchronous. For example:
// Import path is shortened for brevity
import { Resource } from "...";
export default class Home extends Resource {
public paths = ["/"];
public GET(request: Request): Response {
try {
return this.getSomeJSONResponse("GET");
} catch (error) {
return this.handleError(error);
}
}
public POST(request: Request): Response {
try {
return this.getSomeJSONResponse("POST");
} catch (error) {
return this.handleError(error);
}
}
public PUT(request: Request): Response {
try {
return this.getSomeJSONResponse("PUT");
} catch (error) {
return this.handleError(error);
}
}
public DELETE(request: Request): Response {
try {
return this.getSomeJSONResponse("DELETE");
} catch (error) {
return this.handleError(error);
}
}
protected getSomeJSONResponse(httpMethod: string): Response {
// Pretend we did a synchronous operation to get this JSON
const json = JSON.stringify({ result: `Sync ${httpMethod} request done.`});
return new Response(json);
}
protected handleError(e: Error): Response {
return new Response("Fail", { status: 500 });
}
}
Next Steps
Feel free to follow our recommendation, jump ahead, or navigate the documentation pages at your leisure.
Our Recommendations
- Create a tiny HTTP application using the Request Chain module