Skip to main content
The Deno 2 Release Candidate is here
Learn more

deno module deno doc CI codecov vr scripts license

Precise

A clean and easy web server powered by Deno.

Getting started

Minimal example

demo/sample1.ts:

import WebServer from 'https://deno.land/x/precise/mod.ts'

void new WebServer()
  .register({
    path: '/',
    handler: () => ({ foo: 'bar' }),
  })
  .start()

Run the server:

$ deno run demo/sample1.ts
39:043 [Info    ] Logging session initialized. Initial logger min log level: Debug (programmatically set)
39:043 [Info    ] Create web server
39:044 [Info    ] Register 'handler' on route 'GET /'
39:044 [Info    ] Start server
✅ Granted env access to "PORT".
✅ Granted net access to "0.0.0.0:8000".
39:044 [Info    ] Accept connection
39:044 [Info    ] Waiting for new connection
39:044 [Info    ] Web server running. Access it at: http://localhost:8000/
$ █

Request:

$ http :8000
HTTP/1.1 200 OK
content-length: 13
content-type: application/json
date: Wed, 09 Nov 2022 07:02:46 GMT
vary: Accept-Encoding

{
    "foo": "bar"
}

$ █

Server logs:

…
54:006 [Info    ] Handle connection
54:006 [Info    ] Waiting for new request in connection#5
54:006 [Info    ] Waiting for new connection
54:007 [Info    ] Handle request
54:007 [Debug   ] Request 'GET /' matches route 'GET /': apply handler 'handler'
54:008 [Info    ] Waiting for new request in connection#5
54:010 [Debug   ] No more request pending for connection#5
54:010 [Info    ] End processing request in connection#5

Why

This project has been created because of the lack of a stop method in Http Deno and the others third party modules.

I wanted a simple web server service, that starts, registers, and stops, and don’t want to deal with 2 imbricated async iterator loops (serving-http).

Features

  • Basic service lifecycle
  • Route matching
  • Route params

Logger & signals handling

demo/sample2.ts:

import WebServer, { exitOnSignals } from 'https://deno.land/x/precise/mod.ts'

const webServer = new WebServer()
webServer.register({
  path: '/',
  handler: () => ({ foo: 'bar' }),
})
await webServer.start()
exitOnSignals(webServer)

Run the server:

$ deno run demo/sample2.ts
17:393 [Info    ] Logging session initialized. Initial logger min log level: Debug (programmatically set)
17:393 [Info    ] Create web server
17:393 [Info    ] Register 'handler' on route 'GET /'
17:393 [Info    ] Start server
✅ Granted env access to "PORT".
✅ Granted net access to "0.0.0.0:8000".
19:941 [Info    ] Accept connection
19:941 [Info    ] Waiting for new connection
19:941 [Info    ] Web server running. Access it at: http://localhost:8000/
19:942 [Debug   ] Handling signal SIGINT
19:942 [Debug   ] Handling signal SIGTERM
19:942 [Debug   ] Handling signal SIGQUIT
19:942 [Info    ] Type 'kill -s SIGINT 16415' to stop

Stop the server:

$ kill -s SIGINT 16415
$ █

Server logs:

…
25:975 [Warn    ] Received signal SIGINT
25:975 [Info    ] Stop server
25:975 [Warn    ] Listener has been closed
25:975 [Info    ] End processing connection
25:976 [Info    ] Logging session complete.  Duration: 68584ms
$ █

The server is properly stopped without the need to Deno.exit(), so that it can be used cleanly into end-to-end tests.

Route params

Handle routes parameters:

demo/sample3.ts:

import WebServer, { exitOnSignals } from 'https://deno.land/x/precise/mod.ts'

const webServer = new WebServer()
const { logger } = webServer
webServer.register({
  method: 'POST',
  path: '/execute/:cmd',
  handler: (req) => {
    if (req.params?.cmd === 'stop') {
      setTimeout(async () => {
        try {
          await webServer.stop()
        } catch (err) {
          logger.error(err)
          Deno.exit(1)
        }
      }, 1000)
      return new Response(undefined, { status: 202 })
    }
    return { foo: 'bar' }
  },
})

try {
  await webServer.start()
} catch (err) {
  logger.error(err)
  Deno.exit(1)
}

exitOnSignals(webServer)

Request:

$ http post :8000/execute/stop
HTTP/1.1 202 Accepted
content-length: 0
date: Wed, 09 Nov 2022 08:37:53 GMT
vary: Accept-Encoding

$ █

Server logs:

02:742 [Info    ] Handle connection
02:743 [Info    ] Waiting for new request in connection#8
02:743 [Info    ] Waiting for new connection
02:743 [Info    ] Handle request
02:744 [Debug   ] Request 'POST /execute/stop' matches route 'POST /execute/:cmd': apply handler 'handler'
02:744 [Info    ] Waiting for new request in connection#8
02:746 [Debug   ] No more request pending for connection#8
02:746 [Info    ] End processing request in connection#8
03:745 [Info    ] Stop server
03:745 [Warn    ] Listener has been closed
03:745 [Info    ] End processing connection
03:746 [Info    ] Logging session complete.  Duration: 27074ms
$ █

Fallbacks

In case of a not found resource or an error, defaults handler are applied.

Feel free to use custom not found and error handlers.

demo/sample4.ts:

import WebServer from 'https://deno.land/x/precise/mod.ts'

const webServer = new WebServer()
webServer.setErrorHandler((req: Request, err: Error, responseSent: boolean) => {
  if (responseSent) {
    return
  }
  return Response.json(
    {
      code: 'INTERNAL_SERVER',
      message: `Error encountered in request '${req.method} ${req.url}': ${err.message}.`,
    },
    { status: 500 },
  )
})
webServer.setNotFoundHandler((req) =>
  Response.json(
    {
      code: 'NOT_FOUND',
      message: `Resource '${req.method} ${req.url}' not found.`,
    },
    { status: 404 },
  ),
)
webServer.register({
  path: '/oops',
  handler: () => {
    throw new Error('oops')
  },
})

void webServer.start()

Request:

$ http :8000/myverybadroute
HTTP/1.1 404 Not Found
content-encoding: gzip
content-length: 112
content-type: application/json
date: Wed, 09 Nov 2022 15:49:01 GMT
vary: Accept-Encoding

{
  "code": "NOT_FOUND",
  "message": "Resource 'GET http://localhost:8000/myverybadroute' not found."
}

$ http :8000/oops
HTTP/1.1 500 Internal Server Error
content-encoding: gzip
content-length: 123
content-type: application/json
date: Wed, 09 Nov 2022 15:50:11 GMT
vary: Accept-Encoding

{
  "code": "INTERNAL_SERVER",
  "message": "Error encountered in request 'GET http://localhost:8000/oops': oops."
}

$ █

Or in a simpler all-in-one form, as in demo/sample5.ts:

import WebServer from 'https://deno.land/x/precise/mod.ts'

void new WebServer({
  errorHandler: (req: Request, err: Error, responseSent: boolean) => {
    if (responseSent) {
      return
    }
    return Response.json(
      {
        code: 'INTERNAL_SERVER',
        message: `Error encountered in request '${req.method} ${req.url}': ${err.message}.`,
      },
      { status: 500 },
    )
  },
  notFoundHandler: (req: Request) =>
    Response.json(
      {
        code: 'NOT_FOUND',
        message: `Resource '${req.method} ${req.url}' not found.`,
      },
      { status: 404 },
    ),
  handlers: [
    {
      path: '/oops',
      handler: () => {
        throw new Error('oops')
      },
    },
  ],
}).start()

Middlewares

To register a middleware handling all routes, simply omit the path.

demo/sample6.ts:

import WebServer from '../mod.ts'

void new WebServer({
  handlers: [
    {
      handler: function allRoutesHandler(req) {
        this.logger.warn(`The request '${req.method} ${req.url}' was here!`)
        // Do something useful with the request
      },
    },
  ],
}).start()

Request:

$ http :8000/
HTTP/1.1 404 Not Found
content-encoding: gzip
content-length: 71
content-type: text/plain;charset=UTF-8
date: Thu, 10 Nov 2022 09:09:59 GMT
vary: Accept-Encoding

Resource 'GET http://localhost:8000/' not found.

$ █

Server logs:

…
59:727 [Info    ] Handle request
59:727 [Debug   ] Request 'GET /' matches route 'ALL *': apply handler 'allRoutesHandler'
59:728 [Warn    ] The request 'GET http://localhost:8000/' was here!
59:728 [Debug   ] No response sent by routes: fallback to not found handler
…

It’s possible to add a method property to refine matching criteria.

License

The MIT License