Restt is a powerful framework for creating fast and reliable edge worker services.
Crafted for people who love fast development cycles, quality code, security, all while maintaining flexibility of design.
Perfect for building anything from MVP's, to large enterprise applications, to development exercises in the classroom.

Benefits

Enjoy all of these amazing features with Restt

Safe and secure code
Built with zero* package dependencies to ensure your services are safe and secure from any vulnerabilities

Simple external service integration
Easily connect external API services you use like Stripe with Providers

Code flexibility
Shape your code the way you like it with few rules and restrictions

And even more with Restt-CLI

Serve your workers locally
Testing your edge workers services is easily served to you locally with Cloudworker

Auto-compilation and hot-reloading
Automatically recompile and reload your edge worker services on any changes when developing for testing in an instant

Intelligent debugging tools
No more ugly stack traces - enjoy intelligently highlighted error code segments in your terminal

Rapid production distribution
Distribute your edge worker services to people all around the globe instantly with Cloudflare Workers

Installation

Restt is available through the npm registry:

$ npm install restt

Restt-CLI is highly recommended alongside Restt and is also available through the npm registry:

$ npm install -g restt-cli

Usage

Creating a simple hello world service with Restt is as easy as the following:

helloworld-service.js
// Import the required classes from Restt
import { Restt, Service, Resource, Response } from 'restt';
// Create a hello world service
const helloworld = new Service({
  // Define the origin of the service
  origin: 'https://yourdomain.io',
  // Define the list of resources
  resources: [
    // Bind a resource to the service
    new Resource({
      // Define the endpoint (https://yourdomain.io/helloworld)
      endpoint: '/helloworld',
      // Define the resource protocol
      method: 'GET',
      // Define the response of the resource
      response() {
        // Create a response to send to the client
        return new Response({
          // Define the body of the response
          body: {
            message: 'hello world!'
          }
        });
      }
    })
  ]
});
// Create the Restt application
const app = new Restt();
// Bind the service to the application
app.use(helloworld);

If you're using Restt-CLI (and you're all configured) you can now deploy your services locally:

$ restt serve helloworld-service.js

Once served with Restt-CLI you can test your service - let's connect to our resource using curl:

$ curl http://localhost:3000/yourdomain.io/helloworld

If everything is working correctly then curl should output the following:

{
  "message": "helloworld!"
}

Now you're ready to deploy for production with Restt-CLI:

$ restt deploy helloworld-service.js

Congratulations - you've successfully built and shipped your services to the edge using Cloudflare Workers with Restt!

You can check a more detailed overview of the above in the hello world example repository.

Check out the store example repository which includes Stripe and Cloudflare WorkersKV.

API Documentation

Restt follows the ECMA-262 spec alongside the standard service worker spec which work with the V8 JavaScript engine.

Restt

Restt creates an edge worker service application.
Restt can only be instantiated once within any application, but the instance it can have any number of Services bound to it.

class Restt {
  constructor();
  use(service: Service);
}

Service

Service represents a API service which contains a selection of Resources.
Services are deployed to a specified origin.

class Service {
  constructor(service: ServiceConfiguration);
}

Constructor(service)

service

type ServiceConfiguration {
  origin: String,
  headers?: Object,
  passthrough?: Boolean,
  resources: [Resource]
}

service.origin

origin is a required field.
origin must be a String which describes a URL Origin (and path if applicable) which the service will be running on.
origin must not end with a trailing slash.

// A URL Origin
origin: 'https://yourdomain.io'
// A URL Origin including a port
origin: 'https://yourdomain.io:3000'
// A URL Origin including a base path
origin: 'https://yourdomain.io/v1/api'

service.headers

headers is an optional field.
headers must be an Object.
headers should only include keys in lowercase.
headers will always enforce 'content-type': 'application/json'.
headers defines the response headers returned by any request to any resources on this service.

// An example set of response headers (sent to the client)
headers: {
  // Allow CORS from anywhere
  'access-control-allow-origin': '*',
  // Send a flag about the project service version
  'project-service-version': '0.0.1'
}

service.passthrough

passthrough is an optional field.
passthrough must be a Boolean - setting this to true will allow all requests on a service origin (e.g. https://yourdomain.io) to attempt to return their original content (pass through the edge) rather than throw a 404 when a Resource does not match the request url.

service.resources

resources is a required field.
resources must be an array of Resources.
resources must contain at least one resource.

// Create a set of resources (contains only one resource)
resource: [
  new Resource({
    endpoint: '/helloworld',
    method: 'GET',
    response() {
      return new Response({
        body: {
          message: 'hello world!'
        },
        status: 200
      });
    }
  })
]

Resource

Resources are endpoints which run on a Service.
Resources can be statically or dynamically generated depending on their params and fields.
Resources can also be cached using the cache property.

class Resource {
  constructor(configuration: ResourceConfiguration);
}

constructor(resource)

resource

type ResourceConfiguration {
  endpoint: String,
  method: String,
  cache?: (Number | Boolean)
  fields?: [String],
  response: Response
}

resource.endpoint

endpoint is a required field.
endpoint must be a String which describes a URL path (and optionally query paramaters) which the resource can be accessed from.
endpoint must not end with a trailing slash.
endpoint can contain dynamic paramaters if the paramater is wrapped in braces like {param}.

// A simple static endpoint
endpoint: '/helloworld'
// A simple dynamic endpoint
endpoint: '/sqrt/{number}'
// A complex dynamic endpoint
endpoint: '/messages/{user}?search={query}&limit={limit}'

resource.method

method a required field.
method must be a String and be one of the supported methods in Request.

// GET method
method: 'GET'
// POST 
method: 'POST'

resource.cache

origin is an optional field.
origin must be either a Boolean where true represents caching for 60 seconds or a Number which represents the number of seconds to cache for.

// Do not cache
cache: false
// Cache for the default number of seconds
cache: true
// Cache for 1 hour (3600 seconds)
cache: 3600

resource.fields

fields is an optional field.
fields must be an array of String which defines the minimum required fields in the request body.

// A set of fields required in the request body
fields: ['email', 'password']

resource.response(params)

response is a required field.
response must be a function and can be an asynchronous function.
response must return a Response which will be sent to the client when the resource is requested.
response accepts params which will contain any url and query paramaters, fields which are sent in the request, as well as any headers.

// A simple static response
response: () => {
  return new Response({ 
    body: { 
      message: 'hello world',
    },
    status: 200 
  });
}
// A simple dynamic response generating data from paramaters or fields
response: ({ number }) => {
  return new Response({ 
    body: { 
      message: Math.sqrt(number),
    },
    status: 200
  });
}
// A complex dynamic response generating data from paramaters, fields and headers
response: async({ email, password, headers }) => {
  // Check the authorization header (e.g. private internal platform)
  if (!headers.equals('authorization', 'your-auth-token')) {
    // Throw a 403 as the authorization header is incorrect
    return new Response({ status: 403 });
  }
  // Search the database for the customer
  const customer = await Customers.findOne({ email });
  // Check if the customer exists
  if (customer) {
    // Check whether the passwords match
    if (customer.password == password) {
      // Remove the password from the payload
      delete customer.password;
      // Return the customer
      return new Response({ 
        body: { 
          customer,
          success: true
        },
        status: 200
      });
    }
  }
  // Return an error response if there is no customer or invalid credentials
  return new Response({ 
    body: { 
      message: 'Invalid email and password combination',
      success: false
    },
    status: 403
  });
}

Response

Responses are sent to the client when a request to a Resource is made.

class Response {
  constructor(response: ResponseConfiguration);
}

constructor(response)

response

type ResponseConfiguration {
  headers: Object?
  body: Object,
  status: Number
}

response.headers

headers is an optional field.
headers must be an Object.
headers should only include keys in lowercase.
headers will always enforce 'content-type': 'application/json'.
headers defines the response headers for this resource.
headers will extend (and take priority with conflicts) over any headers configured on the service.

// An example set of response headers (sent back to the client)
headers: {
  // Set a cookie for this session
  'set-cookie': 'sessionid=SK302903; Secure'
}

response.body

body is a required field.
body must be an Object.

// An example body
body: {
  message: 'hello world!'
}

response.status

status is a required field.
status must be a Number.
status must be an accepted HTTP response status code.

// An example status(success)
status: 200

Provider

Providers represent external API services which are needed by a Service to form a Response (e.g. Stripe).

stripe-provider.js
// Import the provider module from Restt
import { Provider, Action } from 'restt';
// Export the Stripe provider to be used by a Response
export const Stripe = new Provider({
  // Define the origin for the provider
  origin: 'https://api.stripe.com/v1',
  // Define the headers for the provider (load from the restt.config.json with Restt-CLI)
  headers: configuration.stripe.headers,
  // Add the actions to the providers
  actions: [
    // Create a customer action
    new Action({
      // Define the name for the action
      name: 'createCustomer',
      // Define the endpoint (https://api.stripe.com/v1/customers)
      endpoint: '/customers',
      // Define the method
      method: 'POST',
      // Define the minimum required fields to be called by the function
      fields: ['email']
    })
  ]
});

When instantiated, each action on the Provider will be bound as a function.

// Create a customer with Stripe (async / returns a promise)
Stripe.createCustomer({ email: 'example@yourdomain' });

Providers provide a high level of flexibility for connecting external API services, and allow flow of data to and from external sources without needing to import any service library.
Providers are defined in a similar way to Services, but with the focus on requesting content rather than serving it.
Providers contain a selection of Actions.

class Provider {
  constructor(provider: ProviderConfiguration);
}

constructor(provider)

provider

type ProviderConfiguration {
  origin: String,
  headers?: Object,
  resources: [Resource]
}

provider.origin

origin is a required field.
origin must be a String which describes a URL Origin (and base path if applicable) which the external API service is running on.
origin must not end with a trailing slash.

// A URL Origin
origin: 'https://api.example.io'
// A URL Origin (with a path)
origin: 'https://api.stripe.com/v1'

provider.headers

headers is an optional field.
headers must be an Object.
headers should only include keys in lowercase.
headers defines the request headers for this provider.

// An example set of headers (sent to the external service)
headers: {
    // Add the secret key token as require with Stripe
    'authorization': 'Bearer {your_secret_stripe_key}',
    // Add the content type (must be 'application/json' or 'application/x-www-form-urlencoded')
    'content-type': 'application/x-www-form-urlencoded'
}

provider.actions

actions is a required field.
actions must be an array of Actions.
actions must contain at least one action.
actions defines the action functions which can be called from the provider instance.

// Add the set of actions
actions: [
  new Action({
    name: 'createCustomer',
    endpoint: '/customers',
    method: 'POST',
    fields['email']
  })
]

Actions

Actions are actions or methods which can be called from a Provider.
When actions are called they make Requests to their specified endpoints.
Actions when called return a Promise which contains their response data as specified in the action type or defaulting to JSON

class Action {
  constructor(action: ActionConfiguration);
}

constructor(action)

action

type ActionConfiguration {
  name: String,
  endpoint: String,
  headers: Headers?,
  method: String,
  fields?: [String]
  type?: String
}

action.name

name is always required.
name must be a String and defines a function which can be called on a Provider.

// An example name for an action 
name: 'createCustomer'
// Another example name for an action
name: 'updateCustomer'
// Another example name for an action
name: 'findCustomer'

action.endpoint

endpoint is always required.
endpoint must be a String which describes a URL path (and optionally search params) which the resource can be accessed from.
endpoint must not end with a trailing slash.
endpoint can contain dynamic paramaters if the paramater is wrapped in braces like {param}.

// A static endpoint
endpoint: '/customers'
// A dynamic endpoint
endpoint: '/customers/{id}'
// Anonther dynamic endpoint
endpoint: 'customers?email={email}'

action.headers

headers is an optional field.
headers must be an Object.
headers should only include keys in lowercase.
headers defines the request headers for this action.
headers will extend (and take priority with conflicts) over any headers configured on the provider.

// An example name for an action 
name: 'createCustomer'
// Another example name for an action
name: 'updateCustomer'
// Another example name for an action
name: 'findCustomer'

action.method

method is a required field.
method must be a String and be one of the supported methods in Request.

// POST method
method: 'POST'
// GET method
method: 'GET'

action.fields

fields is an optional field.
fields must be an array of Strings which defines the minimum required fields in the request body.
fields will be passed in the resource response params.

// An example set of fields required by the action
fields: ['email', 'description']

action.type

type is an optional field.
type must be a String and be one of either json(default), text or buffer.

// An example of setting the action response type to be a 'buffer'
type: 'buffer'

Request Headers

headers

Each of the headers from the Request Headers are casted to lowercase and bound to the headers object which contains two helper functions (match and contains).

// An example headers object
headers: {
  'authorization': 'your-secret-key',
  'content-type': 'application/json'
}

headers.equals(header, value)

Checks whether a specific header (e.g. authorization) equals or matches a value (e.g. secret key) and returns true if so.

headers.contains(header, value)

Checks whether a specific header (e.g. authorization) contains a value (e.g. secret key) and returns true if so.

CLI Documentation

Restt-CLI is powerful command line interface crafted to enrich the workflow for developers using Restt or any other framework to develop fast, secure and reliable edge worker services.

Configuration

After installing Restt-CLI, restt.config.jsonwill automatically be added to your project directory
It includes all of the the following default configurations required for running Restt-CLI:

restt.config.json
{
  // Default properties relating to Cloudflare
  "cloudflare": {
    // Cloudflare domain zone (needs to be configured for deployment with WorkersKV)
    "account": ""
    // Cloudflare email address (needs to be configured for deployment)
    "email": "",
    // Cloudflare authentication key (needs to be configured for deployment)
    "key": "",
    // Cloudflare domain zone (needs to be configured for deployment)
    "zone": "",
    // Cloudflare enterprise account (needs to be configured for deployment)
    "enterprise": false,
    // Cloudflare script name (enterpise only - needs to be configured for deployment)
    "script": ""
    // Cloudflare routes to be used (needs to be configured for deployment)
    "routes": []
  },
  // Default properties relating to Cloudworker
  "cloudworker": {
    // Cloudworker debug flag (optional)
    "debug": false,
    // Cloudworker deployment port (needs to be configured for serving)
    "port": 3000
  },
  // Default properties relating to Cloudflare WorkersKV
  "workerskv": {
    // Namespaces to use in Cloudflare WorkersKV (optional)
    "namespaces": []
  }
}

When your edge worker script is served the routes used by services will automatically be added to the cloudflare.routes in your restt.config.json.

You can also configure your cloudflare.routes manually as either an array of strings, or as an object with routes as the values.

// Routes as an array
{
  "cloudflare": {
    ...
    "routes": [
      "https://api.yourdomain.io", 
      "https://api-cache.yourdomain.io"
    ]
  },
  ...
}
// Routes as an object
{
  "cloudflare": {
    ...
    "routes": {
      "api": "https://api.yourdomain.io",
      "cache": "https://api-cache.yourdomain.io"
    }
  },
  ...
}

Routes will always be converted to wildcards when deployed (e.g. https://yourdomain.io becomes https://yourdomain.io/*).

When using Cloudflare WorkersKV the workers.namespaces array should only include the names of any namespaces to be used.
Namespace names should only use alphanumeric characters.

// Using namespaces with WorkersKV
{
  ...
  "workerskv": {
    "namespaces": [
      "namespaceone",
      "namespace2"
    ]
  }
}

All properties in restt.config.json will be bound to the configuration object which is accessible from within your edge worker script which is served or deployed with Restt-CLI.

restt.config.json
{
  "cloudworker": {
    "debug": false,
    "port": 3000
  },
  ...
  "credentials": {
    "authkey": "12345678"
  }
}
example-worker.js
// Outputs "12345678" to the console
console.log(configuration.credentials.authkey);

If you also have a webpack.config.js in your project directory then Restt-CLI will also load and use it when compiling your edge worker script..
You can also add web pack configurations to your restt.config.json which will be prioritised over your webpack.config.js.

restt.config.json
{
  "cloudworker": {
    "debug": false,
    "port": 3000
  },
  ...
  "webpack": {
    "output": {
      "filename": "demo.restt.worker.js"
    }
  }
}

You can also create a restt.production.config.json to configure any values for production and distribution.
Your restt.production.config.json will extend (and overwrite when possible) the values in your standard restt.config.json only when deploying to the edge.

Serving for development

Restt-CLI makes developing with your edge worker services fast and simple.
Automatically build and serve your edge worker service:

$ restt serve [script]

script must point to the path where your edge worker script is located (e.g. src/helloworld-service.js).

While running serve, any modifications to your script will be detected automatically, and your script will be recompiled and hot-reloaded.

Service origins used in Services will be automatically rewritten based on your cloudworker.port. (eg: https://demo.restt.io becomes http://localhost:3000/demo.restt.io)

Deploying to the edge

Restt-CLI can instantly build and distribute your edge worker services in production mode to all over the globe.

Running the following command will automatically build and distribute your edge worker script:

$ restt deploy [script]

script must point to the path where your worker script is located (e.g. src/helloworld-service.js).

When deploying for production your edge worker script will be shipped to Cloudflare Workers using the credentials specified in your configuration.

Restt-CLI will automatically configure any routes and create any WorkersKV namespaces configured in your configuration.

Please ensure that you have setup Cloudflare Workers on your Cloudflare account if you are deploying.

When using Cloudflare WorkersKV, please also ensure that you have WorkersKV access enabled on your account.

Support

Restt is generously supported month to month by these amazing people, teams and organisations!
If you appreciate Restt and would like to regularly support the maintenance of it, please consider becoming a supporter, partner or sponsor.
One-time donations can also be made through PayPal.

Contributing

Please feel free to implement any features by submitting a pull request for Restt, or Restt-CLI.
Alternatively, you can submit feature request for either Restt or Restt-CLI.
All contributions are greatly appreciated!

Security

Restt has been built with ultimate security in mind and has as zero dependencies to mitigate most risks, vulnerabilities and exploits which can come with using third-party packages.

While Restt has zero dependencies, Restt-CLI which is highly recommended alongside Restt contains both Webpack and Cloudworker as dependencies.

At present it is incredibly difficult to create software which uses JavaScript packages or modules without a compilation tool such as Webpack

Likewise, it is a difficult task to deploy edge workers locally for development and testing without a runner like Cloudworker.

Webpack and Cloudworker are both made by highly respectable developers, teams and organisations who also are very security conscious.

No package or module can guarantee complete security of your code and any data which passes through it.

License

MIT
Copyright (c) 2019-present, Daniel Larkin