pogo
Server framework for Deno
Pogo is an easy-to-use, safe, and expressive framework for writing web servers and applications. It is inspired by hapi.
Supports Deno v1.0.0 and higher.
Contents
Why?
- Designed to encourage reliable and testable applications.
- Routes are simple, pure functions that return values directly.
- Automatic JSON responses from objects.
- Built-in support for React and JSX.
Usage
Save the code below to a file named server.js
and run it with a command like deno --allow-net server.js
. Then visit http://localhost:3000 in your browser and you should see âHello, world!â on the page.
import pogo from 'https://deno.land/x/pogo/main.ts';
const server = pogo.server({ port : 3000 });
server.router.get('/', () => {
return 'Hello, world!';
});
server.start();
The examples that follow will build on this to add more capabilities to the server. Some advanced features may require additional permission flags or different file extensions. If you get stuck or need more concrete examples, be sure to check out the example projects.
Adding routes
A route matches an incoming request to a handler function that creates a response
Adding routes is easy, just call server.route()
and pass it a single route or an array of routes. You can call server.route()
multiple times. You can even chain calls to server.route()
, because it returns the server instance.
Add routes in any order you want to, itâs safe! Pogo orders them internally by specificity, such that their order of precedence is stable and predictable and avoids ambiguity or conflicts.
server.route({ method : 'GET', path : '/hi', handler : () => 'Hello!' });
server.route({ method : 'GET', path : '/bye', handler : () => 'Goodbye!' });
server
.route({ method : 'GET', path : '/hi', handler : () => 'Hello!' });
.route({ method : 'GET', path : '/bye', handler : () => 'Goodbye!' });
server.route([
{ method : 'GET', path : '/hi', handler : () => 'Hello!' },
{ method : 'GET', path : '/bye', handler : () => 'Goodbye!' }
]);
You can also configure the route to handle multiple methods by using an array, or '*'
to handle all possible methods.
server.route({ method : ['GET', 'POST'], path : '/hi', handler : () => 'Hello!' });
server.route({ method : '*', path : '/hi', handler : () => 'Hello!' });
Serve static files
You can use Deno.readFile()
to serve the contents of a file from the filesystem.
Note that currently you should specify the content type of the file using response.type()
. Future versions of Pogo will provide more egornomic APIs for static files that handle this automatically.
server.router.get('/', async (request, h) => {
const buffer = await Deno.readFile('./hello.jpg');
return h.response(buffer).type('image/jpeg');
});
React and JSX support
JSX is a shorthand syntax for JavaScript that looks like HTML and is useful for constructing web pages
You can do webpage templating with React inside of route handlers, using either JSX or React.createElement()
.
Pogo automatically renders React elements using ReactDOMServer.renderToStaticMarkup()
and sends the response as HTML.
Save the code below to a file named server.jsx
and run it with a command like deno --allow-net server.jsx
. The .jsx
extension is important, as it tells Deno to compile the JSX syntax. You can also use TypeScript by using .tsx
instead of .jsx
. The type definitions should load automatically from the Pika CDN, but if you run into problems when using .tsx
, try loading them manually (see deno_types).
import React from 'https://cdn.pika.dev/react';
import pogo from 'https://deno.land/x/pogo/main.ts';
const server = pogo.server({ port : 3000 });
server.router.get('/', () => {
return <h1>Hello, world!</h1>;
});
server.start();
Writing tests
When it comes time to write tests for your app, Pogo has you covered with server.inject()
.
By injecting a request into the server directly, we can completely avoid the need to listen on an available port, make HTTP connections, and all of the problems and complexity that come along with that. You should focus on writing your application logic and server.inject()
makes that easier.
The server still processes the request using the same code paths that a normal HTTP request goes through, so you can rest assured that your tests are meaningful and realistic.
const response = await server.inject({
method : 'GET',
url : '/users'
});
API
pogo.server(options)
pogo.router(options?)
- Server
- Request
- Response
response.body
response.code(statusCode)
response.created(url?)
response.header(name, value)
response.headers
response.location(url)
response.permanent()
response.redirect(url)
response.rewritable(isRewritable?)
response.state(name, value)
response.status
response.temporary()
response.type(mediaType)
response.unstate(name)
- Response Toolkit
- Router
router.add(route, options?, handler?)
router.all(route, options?, handler?)
router.delete(route, options?, handler?)
router.get(route, options?, handler?)
router.lookup(method, path)
router.patch(route, options?, handler?)
router.post(route, options?, handler?)
router.put(route, options?, handler?)
router.routes
pogo.server(options)
Returns a Server
instance, which can then be used to add routes and start listening for requests.
const server = pogo.server();
options
Type: object
catchAll
Type: function
Optional route handler to be used as a fallback for requests that do not match any other route. This overrides the default 404 Not Found behavior built into the framework. Shortcut for server.router.all('/{catchAll*}', catchAll)
.
pogo.server({
catchAll(request, h) {
return h.response('the void').code(404);
}
});
certFile
Type: string
Example: '/path/to/file.cert'
Optional filepath to an X.509 public key certificate for the server to read when server.start()
is called, in order to set up HTTPS. Requires the use of the keyFile
option.
hostname
Type: string
Default: 'localhost'
Optional domain or IP address for the server to listen on when server.start()
is called. Use '0.0.0.0'
to listen on all available addresses, as mentioned in the security documentation.
keyFile
Type: string
Example: '/path/to/file.key'
Optional filepath to a private key for the server to read when server.start()
is called, in order to set up HTTPS. Requires the use of the certFile
option.
port
Type: number
Example: 3000
Any valid port number (0
to 65535
) for the server to listen on when server.start()
is called. Use 0
to listen on an available port assigned by the operating system.
pogo.router(options?)
Returns a Router
instance, which can then be used to add routes.
const router = pogo.router();
Server
The server
object returned by pogo.server()
represents your web server. When you start the server, it begins listening for HTTP requests, processes those requests when they are received, and makes the content within each request available to the route handlers that you specify.
server.inject(request)
Performs a request directly to the server without using the network. Useful when writing tests, to avoid conflicts from multiple servers trying to listen on the same port number.
Returns a Promise
for a Response
instance.
const response = await server.inject({
method : 'GET',
url : '/'
});
request
Type: object
method
Type: string
Example: 'GET'
Any valid HTTP method, such as GET
or POST
. Used to lookup the route handler.
url
Type: string
Example: '/'
Any valid URL path. Used to lookup the route handler.
server.route(route, options?, handler?)
Adds a route to the server so that the server knows how to respond to requests that match the given HTTP method and URL path. Shortcut for server.router.add()
.
Returns the server so other methods can be chained.
server.route({ method : 'GET', path : '/', handler : () => 'Hello, World!' });
server.route({ method : 'GET', path : '/' }, () => 'Hello, World!');
server.route('/', { method : 'GET' }, () => 'Hello, World!');
route
Type: object
| string
| Router
| Array<object | string | Router>
method
Type: string
| Array<string>
Example: 'GET'
Any valid HTTP method, array of methods, or '*'
to match all methods. Used to limit which requests will trigger the route handler.
path
Type: string
Example: '/users/{userId}'
Any valid URL path. Used to limit which requests will trigger the route handler.
Supports path parameters with dynamic values, which can be accessed in the handler as request.params
.
handler(request, h)
Type: function
request
is aRequest
instance with properties forheaders
,method
,url
, and more.h
is a Response Toolkit instance, which has utility methods for modifying the response.
The implementation for the route that handles requests. Called when a request is received that matches the method
and path
specified in the route options.
The handler must return one of:
- A
string
, which will be sent as HTML. - An
object
, which will be stringified and sent as JSON. - A
Uint8Array
, which will be sent as-is (raw bytes). - A
Response
, which will send theresponse.body
, if any. - Any object that implements the
Reader
interface, such as aFile
orBuffer
. - An
Error
, which will send an appropriate HTTP error code - returning an error is the same asthrow
ing it. - A
Promise
for any of the above types.
An appropriate Content-Type
header will be set automatically based on the response body before the response is sent. You can use response.type()
to override the default behavior.
server.router
Type: Router
The route manager for the server, which contains the routing table for all known routes, as well as various methods for adding routes to the routing table.
server.start()
Begins listening for requests on the hostname
and port
specified in the server options.
Returns a Promise
that resolves when the server is listening.
await server.start();
console.log('Listening for requests');
server.stop()
Stops accepting new requests. Any existing requests that are being processed will not be interrupted.
Returns a Promise
that resolves when the server has stopped listening.
await server.stop();
console.log('Stopped listening for requests');
Request
The request
object passed to route handlers represents an HTTP request that was sent to the server. It is similar to an instance of Denoâs ServerRequest
class, with some additions.
It provides properties and methods for inspecting the content of the request.
request.body
Type: Reader
The HTTP body value.
You can read raw bytes from the body in chunks with request.body.read()
, which takes a Uint8Array
as an argument to copy the bytes into and returns a Promise
for either the number of bytes read or null
when the body is finished being read.
const decoder = new TextDecoder();
const buffer = new Uint8Array(20);
const numBytesRead = await request.body.read(buffer);
const bodyText = decoder.decode(buffer.subarray(0, numBytesRead));
To get the body as a string, pass it to Deno.readAll()
and decode the result, as shown below. Note that doing so will cause the entire body to be read into memory all at once, which is convenient, but may be inappropriate for requests with a very large body.
const decoder = new TextDecoder();
const bodyText = decoder.decode(await Deno.readAll(request.body));
request.headers
Type: Headers
Contains the HTTP headers that were sent in the request, such as Accept
, User-Agent
, and others.
request.host
Type: string
Example: 'localhost:3000'
The value of the HTTP Host
header, which is a combination of the hostname and port at which the server received the request, separated by a :
colon. Useful for returning different content depending on which URL your visitors use to access the server. Shortcut for request.url.host
.
To get the hostname, which does not include the port number, see request.hostname
.
request.hostname
Type: string
Example: 'localhost'
The hostname part of the HTTP Host
header. That is, the domain or IP address at which the server received the request, without the port number. Useful for returning different content depending on which URL your visitors use to access the server. Shortcut for request.url.hostname
.
To get the host, which includes the port number, see request.host
.
request.href
Type: string
Example: 'http://localhost:3000/page.html?query'
The full URL associated with the request, represented as a string. Shortcut for request.url.href
.
To get this value as a parsed object instead, use request.url
.
request.method
Type: string
Example: 'GET'
The HTTP method associated with the request, such as GET
or POST
.
request.origin
Type: string
Example: 'http://localhost:3000'
The scheme and host parts of the request URL. Shortcut for request.url.origin
.
request.params
Type: object
Contains the name/value pairs of path parameters, where each key is a parameter name from the route path and the value is the corresponding part of the request path. Shortcut for request.route.params
.
request.path
Type: string
Example: /page.html
The path part of the request URL, excluding the query. Shortcut for request.url.pathname
.
request.raw
Type: ServerRequest
The original request object from Denoâs http
module, upon which many of the other request properties are based.
You probably donât need this. It is provided as an escape hatch, but using it is not recommended.
request.referrer
Type: string
The value of the HTTP Referer
header, which is useful for determining where the request came from. However, not all user agents send a referrer and the value can be influenced by various mechanisms, such as Referrer-Policy
. As such, it is recommended to use the referrer as a hint, rather than relying on it directly.
Note that this property uses the correct spelling of âreferrerâ, unlike the header. It will be an empty string if the header is missing.
request.response
Type: Response
The response that will be sent for the request. To create a new response, see h.response()
.
request.route
Type: object
The route that is handling the request, as given to server.route()
, with the following additional properties:
params
is an object with properties for each dynamic path parametersegments
is an array of path parts, as in the values separated by/
slashes in the route path
request.search
Type: string
Example: '?query'
The query part of the request URL, represented as a string. Shortcut for request.url.search
.
To get this value as a parsed object instead, use request.searchParams
.
request.searchParams
Type: URLSearchParams
The query part of the request URL, represented as an object that has methods for working with the query parameters. Shortcut for request.url.searchParams
.
To get this value as a string instead, use request.search
.
request.server
Type: Server
The server that is handling the request.
request.state
Type: object
Contains the name/value pairs of the HTTP Cookie
header, which is useful for keeping track of state across requests, e.g. to keep a user logged in.
request.url
Type: URL
The full URL associated with the request, represented as an object that contains properties for various parts of the URL,
To get this value as a string instead, use request.href
. In some cases, the URL object itself can be used as if it were a string, because it has a smart .toString()
method.
Response
The response
object represents an HTTP response to the associated request
that is passed to route handlers. You can access it as request.response
or create a new response with the Response Toolkit by calling h.response()
. It has utility methods that make it easy to modify the headers, status code, and other attributes.
response.body
Type: string
| object
| Uint8Array
| Reader
The body that will be sent in the response. Can be updated by returning a value from the route handler or by creating a new response with h.response()
and giving it a value.
response.code(statusCode)
Sets the response status code. When possible, it is better to use a more specific method instead, such as response.created()
or response.redirect()
.
Returns the response so other methods can be chained.
Tip: Use Denoâs status
constants to define the status code.
import { Status as status } from 'https://deno.land/std/http/http_status.ts';
const handler = (request, h) => {
return h.response().code(status.Teapot);
};
response.created(url?)
Sets the response status to 201 Created
and sets the Location
header to the value of url
, if provided.
Returns the response so other methods can be chained.
response.header(name, value)
Sets a response header. Always replaces any existing header of the same name. Headers are case insensitive.
Returns the response so other methods can be chained.
response.headers
Type: Headers
Contains the HTTP headers that will be sent in the response, such as Location
, Vary
, and others.
response.location(url)
Sets the Location
header on the response to the value of url
. When possible, it is better to use a more specific method instead, such as response.created()
or response.redirect()
.
Returns the response so other methods can be chained.
response.permanent()
Only available after calling the response.redirect()
method.
Sets the response status to 301 Moved Permanently
or 308 Permanent Redirect
based on whether the existing status is considered rewritable (see âmethod handlingâ on Redirections in HTTP for details).
Returns the response so other methods can be chained.
response.redirect(url)
Sets the response status to 302 Found
and sets the Location
header to the value of url
.
Also causes some new response methods to become available for customizing the redirect behavior:
Returns the response so other methods can be chained.
response.rewritable(isRewritable?)
Only available after calling the response.redirect()
method.
Sets the response status to 301 Moved Permanently
or 302 Found
based on whether the existing status is a permanent or temporary redirect code. If isRewritable
is false
, then the response status will be set to 307 Temporary Redirect
or 308 Permanent Redirect
.
Returns the response so other methods can be chained.
response.state(name, value)
Sets the Set-Cookie
header to create a cookie with the given name
and value
. Cookie options can be specified by using an object for value
. See Denoâs cookie interface for the available options.
Returns the response so other methods can be chained.
All of the following forms are supported:
response.state('color', 'blue');
response.state('color', { value : 'blue' });
response.state({ name : 'color', value : 'blue' });
response.status
Type: number
Example: 418
The status code that will be sent in the response. Defaults to 200
, which means the request succeeded. 4xx and 5xx codes indicate an error.
response.temporary()
Only available after calling the response.redirect()
method.
Sets the response status to 302 Found
or 307 Temporary Redirect
based on whether the existing status is considered rewritable (see âmethod handlingâ on Redirections in HTTP for details).
Returns the response so other methods can be chained.
response.type(mediaType)
Sets the Content-Type
header on the response to the value of mediaType
.
Overrides the media type that is set automatically by the framework.
Returns the response so other methods can be chained.
response.unstate(name)
Sets the Set-Cookie
header to clear the cookie given by name
.
Returns the response so other methods can be chained.
Response Toolkit
The response toolkit is an object that is passed to route handlers, with utility methods that make it easy to modify the response. For example, you can use it to set headers or a status code.
By convention, this object is assigned to a variable named h
in code examples.
h.redirect(url)
Creates a new response with a redirect status. Shortcut for h.response().redirect(url)
. See response.redirect()
for details.
Returns the response so other methods can be chained.
h.response(body?)
Creates a new response with an optional body. This is the same as returning the body directly from the route handler, but it is useful in order to begin a chain with other response methods.
Returns the response so other methods can be chained.
Router
Documentation: Routing
A router is used to store and lookup routes. The server has a built-in router at server.router
, which it uses to match an incoming request to a route handler function that generates a response. You can use the serverâs router directly or you can create a custom router with pogo.router()
.
To copy routes from one router to another, see router.add()
. You can pass a custom router to server.route()
or server.router.add()
to copy its routes into the serverâs built-in router, thus making those routes available to incoming requests.
Note that you donât necessarily need to create a custom router. You only need to create your own router if you prefer the chaining syntax for defining routes and you want to export the routes from a file that doesnât have access to the server. In other words, a custom router is useful for larger applications.
const server = pogo.server();
server.router
.get('/', () => {
return 'Hello, World!';
})
.get('/status', () => {
return 'Everything is swell!';
});
const router = pogo.router()
.get('/', () => {
return 'Hello, World!';
})
.get('/status', () => {
return 'Everything is swell!';
});
const server = pogo.server();
server.route(router);
router.add(route, options?, handler?)
Adds one or more routes to the routing table, which makes them available for lookup, e.g. by a server trying to match an incoming request to a handler function.
The route
argument can be:
- A route object with optional properties for
method
,path
, andhandler
method
is an HTTP method string or array of stringspath
is a URL path stringhandler
is a function
- A string, where it will be used as the path
- A
Router
instance, where its routing table will be copied - An array of the above types
The options
argument can be a route object (same as route
) or a function, where it will be used as the handler.
The handler
function can be a property of a route
object, a property of the options
object, or it can be a standalone argument.
Each argument has higher precedence than the previous argument, allowing you to pass in a route but override its handler, for example, by simply passing a handler as the final argument.
Returns the router so other methods can be chained.
const router = pogo.router().add('/', { method : '*' }, () => 'Hello, World!');
router.all(route, options?, handler?)
Shortcut for router.add()
, with '*'
as the default HTTP method.
Returns the router so other methods can be chained.
const router = pogo.router().all('/', () => 'Hello, World!');
router.delete(route, options?, handler?)
Shortcut for router.add()
, with 'DELETE'
as the default HTTP method.
Returns the router so other methods can be chained.
const router = pogo.router().delete('/', () => 'Hello, World!');
router.get(route, options?, handler?)
Shortcut for router.add()
, with 'GET'
as the default HTTP method.
Returns the router so other methods can be chained.
const router = pogo.router().get('/', () => 'Hello, World!');
router.lookup(method, path)
Look up a route that matches the given method
and path
.
Returns the route object with an additional params
property that contains path parameter names and values.
router.patch(route, options?, handler?)
Shortcut for router.add()
, with 'PATCH'
as the default HTTP method.
Returns the router so other methods can be chained.
const router = pogo.router().patch('/', () => 'Hello, World!');
router.post(route, options?, handler?)
Shortcut for router.add()
, with 'POST'
as the default HTTP method.
Returns the router so other methods can be chained.
const router = pogo.router().post('/', () => 'Hello, World!');
router.put(route, options?, handler?)
Shortcut for router.add()
, with 'PUT'
as the default HTTP method.
Returns the router so other methods can be chained.
const router = pogo.router().put('/', () => 'Hello, World!');
router.routes
Type: object
The routing table, which contains all of the routes that have been added to the router.
Contributing
See our contributing guidelines for more details.
- Fork it.
- Make a feature branch:
git checkout -b my-new-feature
- Commit your changes:
git commit -am 'Add some feature'
- Push to the branch:
git push origin my-new-feature
- Submit a pull request.
License
Go make something, dang it.