Introduction
Web 2.0 framework to make building backends and APIs easy and fun:
- HTTP APIs, WebSocket, Web Push (coming), FIFO queues, and scheduled jobs
- Focus on developer experience and simplicity
- Serverless: deploy and let it worry about scaling up/down
- Made for the web: REST resources, Fetch API, HTML forms, console.log
- Batteries included: logging, authentication, custom domains, URL constructors
- No need to mess CloudFormation or edit YAML files
- TypeScript and ESM (JavaScript and CommonJS also supported)
Why QueueRun?
AWS Lambda has all the right building blocks — HTTPS, WebSocket, SQS queues, CloudWatch logs — but the developer experience is not there. I wanted a framework that can go from idea to deploy in minutes not weeks.
Next, Remix, Nuxt, et al solves that for developing front-end applications. I wanted something as easy and fun for building the backend: the APIs, presence and real-time updates (WebSocket), queued and scheduled jobs, etc.
Every backend needs authentication, logging, environment variables, URL construction, etc. The framework should take care of that.
Deployment should take less than 2 minutes. Setting up a new project in under 5 minutes. Don't want to worry about provisioning servers, scaling up/down, CloudFormation, or that thing they call YAML.
Above all, the developer experience! Common tasks should be as easy as writing a few lines of code. Whether you're building a REST API, real time collaboration (WebSocket), responsive UIs (queues), running background tasks on a schedule.
See An Example
Let's install queue-run. We need the command line tool, and types library, so we'll install as dev dependency:
npm install -D queue-run
yarn add --dev queue-run
HTTP Requests
Next we'll write a simple backend. Start with a resource for listing all bookmarks (GET) and creating a new bookmark (POST):
Clone Our Example
You can also clone the repo and look at the packages/example directory.
import { queue as screenshots } from "#queues/screenshots.js";
import { urlForBookmark } from "./[id].js";
import db from "#lib/db.js";
// HTTP GET /bookmarks -> JSON
export async function get({ user }) {
return await db.bookmarks.findAll({ userId: user.id });
}
// And this is HTTP POST -> 303 See Other
export async function post({ body, user }) {
const { title, url } = body;
const bookmark = await db.bookmarks.create({ title, url, user });
await screenshots.push({ id: bookmark.id });
// This will generate a URL like
// https://example.com/bookmarks/c675e615%
const newURL = urlForBookmark(bookmark);
return Response.redirect(newURL, 303);
}
JavaScript, TypeScript, and ESM
You can write your backend in JavaScript, TypeScript, or combination of both.
All the examples are in TypeScript, to illustrate how you can use type checks.
ESM imports must end with the filename extension .js
(or .jsm
).
Learn more about using TypeScript and ESM.
You can also fetch (GET), update (PUT), and delete (DELETE) an individual resource:
import { url } from "queue-run";
import db from "#lib/db.js";
// In Express this would be get('/bookmarks/:id')
export async function get({ params, user }) {
const bookmark = await db.bookmarks.findOne({
id: params.id,
userId: user.id
});
// Throw a response to exit request handling early
if (!bookmark) throw new Response(null, { status: 404 });
return bookmark;
}
export async function put({ body, params, user }) {
const bookmark = await db.bookmarks.findOne({
id: params.id,
userId: user.id
});
if (!bookmark) throw new Response(null, { status: 404 });
const { title } = body;
return await db.bookmarks.updateOne({ id: params.id, title });
}
export async function del({ params, user}) {
await db.deleteOne({
id: params.id,
userId: user.id
});
return new Response(null, { status: 204 });
}
// index.ts uses this to create URLs
export const urlForBookmark = url.self<{ id: string }>();
Learn more about handling HTTP requests and routing.
Authentication
We'll need some common middleware to authenticate requests, so we can tie them to a user:
import { form } from "queue-run";
export async function authenticate({ bearerToken }) {
const profile = await jwt.verify({
token: bearerToken,
secret: process.env.JWT_SECRET
});
const user = await users.findOne(profile.sub);
if (!user) throw new Response("No such user", { status: 403 });
return user;
}
Learn more about authentication.
Queues
Our bookmarks service takes screenshots, and these could take several seconds, and even fail intermittently. We'll use a queue for that:
import { queues, socket } from "queue-run";
import db from "#lib/db.js";
import capture from "#lib/capture.js";
export default async function ({ id }, { user }) {
const bookmark = await db.bookmarks.findOne({ id, userId: user.id });
if (!bookmark) return;
// This could easily take several seconds,
// so we're doing this in a background job
console.info("Taking screenshot of %s", bookmark.url)
const screenshot = await capture(bookmark.url);
await db.bookmarks.updateOne({ id, userId: user.id, screenshot });
// If the client uses WebSocket, let them know we updated the bookmark
await socket.push({ update: 'bookmark', id });
}
// api/bookmarks/index.ts doesn't need to guess the queue name
//
// Type information for your IDE
export const queue = queues.self<{ id: string }>();
Learn more about standard and FIFO queues.
WebSocket
In this example we're using WebSocket to notify the browser when we're done capturing the screenshot.
So we only need two pieces of code. From the browser, open a WebSocket connection and authenticate it:
const ws = new WebSocket("wss://ws.grumpy-sunshine.queue.run");
// Connection opens, we immediately attempt to authenticate
ws.onopen = () => ws.send({ jwtToken });
// Wait for the server to either accept (message) or deny (close socket)
await new Promise((resolve, reject) => {
ws.onmessage = resolve;
ws.onclose = reject;
});
From the server, handle the authentication request and acknowledge it:
export async function authenticate({ data }) {
try {
const profile = await jwt.verify({
token: data.token,
secret: process.env.JWT_SECRET
});
return await users.findOne(profile.sub);
return { id: sub, email };
} catch {
// Reject by closing the WebSocket
await socket.close();
}
}
Learn more about WebSocket.
Use Locally
Let's run this backend using the development server:
npx queue-run dev
👋 Dev server listening on:
http://localhost:8000
ws://localhost:8000
In another terminal window we're going to create a new bookmark, retrieve that bookmark, and list all the bookmarks:
curl http://localhost:8000/bookmarks -X POST \
-F "title=My bookmark" -F "url=http://example.com"
curl http://localhost:8000/bookmarks/74e83d43
curl http://localhost:8000/bookmarks
Install Dependencies
To speed up npx queue-run
you can install these two development dependencies:
npm install -D queue-run queue-run-cli
yarn add -D queue-run queue-run-cli
Deploy To Production
We'll start by initializing the project. You only need to do this once, when you start working on a new project (recommended) or before the first deploy.
It will ask you a few question: project name, JavaScript or TypeScript, runtime. Then fill in any missing files.
And then we deploy!
npx queue-run deploy
You'll see the URL for your new backend. You can try and make HTTP requests against it, open WebSocket connection, etc.
Learn more about commands to deploy your code, setup custom domains, watch logs, rollback, and more.
tip
If you used our example, then you can open the URL in your browser, and it will show you curl
commands for testing your the backend.