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:

src/core/http/Resource.ts
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 ...

MyResource.ts
 // 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:

MyResource.ts
// 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:

Synchronous/Asynchronous

Your resources' HTTP request methods can be synchronous or asynchronous. For example:

Home.ts
// 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