Quick Start
One page summary of how to start a new Pioche project.
Introduction to Workers
Read Intro to Workers for an introduction to the workers ecosystem.
Install the Workers CLI
First, ensure you have node.js and npm installed. We recommend using a node version manager like nvm to make changing versions and upgrading easier.
Now install Cloudflare Wrangler (the Workers CLI tool) globally
$ npm install -g wrangler
Initialize a New Project
We can use pioche-scripts to create a fully set up pioche project in a single command using the npm package runner npx. This will create a directory called YourAppName in the current working directory.
$ npx pioche-scripts create YourAppName
$ cd YourAppName
After creating the project, you will be prompted to login to Cloudflare, if you don’t have an account, you can make one at this step or just skip for now (ctrl+c
to cancel). To login later, just run
$ wrangler login
Running the Basic Project
If you’ve logged into wrangler, we can deploy our project to Cloudflare using
$ npm run deploy
Then the app will be available at yourappname.workers.dev
where you can see the Hello, World!
response. (If your Cloudflare account has a default zone configured, the Worker will be available at yourappname.zone.workers.dev
)
If you have not logged into Wrangler, you can still run the app locally using
$ npm run serve
Pioche App Structure
There are only two restrictions on file structure for a Pioche project:
- Everything must be located in
src/
- You cannot create a file called
entry.ts
A Pioche project consists of:
Handlers: Like middleware in other frameworks, adds functionality on the project or route level to be executed before (preHandler) or after (postHandler) the mapped controller handler.
Controllers: Classes that define mapped handlers using mapping decorators.
Views: Objects or classes defining structure for data
We also have our pioche.config.js file at the project root. Here, we can define app level functionality such as preHandlers, postHandlers, external controllers (from packages), and kv_namespaces.
Each incoming request is first parsed into Session
and OutboundResponse
objects. If the incoming request matches a mapped handler, these objects will then be passed around as follows:
- App level preHandlers (defined in
pioche.config.js
preHandlers section) - Route level preHandlers (defined using
@UseBefore()
) - Mapped Controller handler
- Route level postHandlers (defined using
@UseAfter()
) - App level postHandlers (defined in
pioche.config.js
postHandlers section)
Adding Functionality
The default pioche app has a single file at src/controllers/helloworld.ts
(copied below). This contains a controller class which adds a single endpoint to your workers application that will give a Hello, World!
response at the base domain.
import { BaseMap, GetMap, WorkerController, OutboundResponse, Session } from "pioche";
@BaseMap("")
export class HelloWorldController extends WorkerController {
@GetMap("")
async helloWorld(session: Session, res: OutboundResponse){
res.body = "Hello, World!";
}
}
Environments
See controllers for full details
Three environments are currently supported by extending our Controller class.
export class Controller1 extends WorkerController{}
export class Controller2 extends DurableObjectController{}
export class Controller3 extends WebsocketController{}
Worker controllers execute their code on the worker, Durable Object controllers and websocket controllers execute their code on a durable object defined by the controller class, and websocket controllers have extra features included to make working with websockets easier. If we are making calls to Durable Object storage, it is both faster and cheaper to run the associated code on the Durable Object iself. With Pioche, this is possible by just changing the environment of the controller.
Routing
See routing for full details
Let’s say you want to add a new mapping to the app to say hello to anyone. The routing system uses path-to-regexp behind the scenes so we can just add a path parameter as follows to a new handler within our controller
...
@GetMap("/:name")
async helloName(session: Session, res: OutboundResponse){
res.body = `Hello, ${session.request.params.name}!`;
}
...
After deploying or running locally you can navigate to /Johnny
to see Hello, Johnny!
.
All path-to-regexp features are supported so check their docs to implement further flexibility. @BaseMap("/hello")
with @PostMap("/:name")
is accessible with a POST request to /hello/Johnny
this is a simple concatenation of the two routes. A request with a trailing /
can match a route without a trailing /
but not vice versa. All HTTP verbs are supported as well as @AnyMap(<route>)
to match any method.
Durable Objects have a special routing property allowing you specify the name, hex id, or DurableObjectId
given the request as shown below.
import { BaseMap, GetMap, WorkerController, OutboundResponse, Session, DOTarget } from "pioche";
function targeter(session: Session, res: OutboundResponse, targetNS: DurableObjectNamespace): DOTarget{
// We could also specify a hex 'idstring' or a DurableObjectID 'id'
return {name: session.request.params.name}
}
// Since our targeter uses params.name, it should be part of the BaseMap
@BaseMap("/:name")
// Now tell our requests go to a durable object with name = params.name
@TargetDO(targeter)
export class HelloWorldController extends WorkerController {
@GetMap("")
async helloWorld(session: Session, res: OutboundResponse){
res.body = `Hello, ${session.request.params.name}!`;
}
}
Storage
See storage for full details.
Pioche wraps the native storage APIs to unify the Durable Object and KV storage syntaxes and allow object-like use of remote storage. Using this new API we can use chaining (optional chaining not supported) to access and write to our storages as shown below
import { BaseMap, GetMap, WorkerController, OutboundResponse, Session, }from "pioche";
@BaseMap("/:name")
export class HelloWorldController extends WorkerController {
@GetMap("/config")
async getUserConfig(session: Session, res: OutboundResponse){
// Asynchronously retrieve the user's config using chaining
res.body = this.storage[session.request.params.name].get();
}
@GetMap("/regdate")
async getUserAge(session: Session, res: OutboundResponse){
// Asynchronously retrieve the user's config using chaining
res.body = this.storage[session.request.params.name].regdate.get();
}
@PutMap("/config")
async putUserConfig(session: Session, res: OutboundResponse){
if(await this.storage.get(session.request.params.name))
this.storage[session.request.params.name] = session.request.json();
res.body = "Updated user config if exists";
}
@PostMap("/config")
async addUserConfig(session: Session, res: OutboundResponse){
this.storage[session.request.params.name] = {regdate: new Date()};
res.body = "Added new user config";
}
}
Views
See views for full details.
We can define intended structures for our data using the view and checks component of Pioche. A view defines the structure of data, a check validates a piece of data. We could define a view as follows to validate usernames, emails, and age
class UserView{
// Checks for 5-20 alphanumeric characters
username = and(lenbt(5, 20), rx(/^[a-zA-Z0-9]+$/));
// Checks for email regex
email = rx(/^[^@][email protected][^@]+\.[^@]+$/);
// Not required, but if provided must be >13
age = optional(gt(13));
}
Then we can apply that view to some incoming data. The View function returns an instance of the passed object/class with the checks replaced with the found values. The returned object also has:
getStatus()
: Returns 0 if keys were missing or failing checks and 1 if everything passedgetMissing()
: Returns a list of missing keys or paths to missing keys, i.e. ifCVV
was missing from a sub-objectcreditCard
it will return[["creditCard", "CVV"]]
getFailing()
: Returns a list of keys or paths to keys which failed their respective checks.
...
@PostMap("/register/:name")
async register({request}, res){
if(View(request.json(), UserView).getStatus() === 0){
this.storage[request.params.name] = request.json();
res.body = "Successfully registered";
} else {
res.body = "Registration failed"
}
}
...