Skip to main content
Deno 2 is finally here πŸŽ‰οΈ
Learn more

deno module build codecov vr scripts

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: () => Response.json({ foo: 'bar' }),
      })
      .start()
  • Run the server:

    $ deno run demo/sample1.ts
    35:511 [Info    ] Logging session initialized. Initial logger min log level: Debug (programmatically set)
    35:511 [Info    ] Create API server
    35:511 [Info    ] Register 'handler' with route GET /
    35:512 [Info    ] Start server
    βœ… Granted env access to "PORT".
    βœ… Granted net access to "0.0.0.0:8000".
    37:869 [Info    ] Apply server request handler 'handler'
    37:869 [Info    ] Apply server request handler 'defaultNotFoundRequestHandler'
    37:869 [Info    ] Accept connection
    37:869 [Info    ] Waiting for new connection
    37:869 [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"
    }
    
    $ β–ˆ

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: () => Response.json({ foo: 'bar' }),
    })
    await webServer.start()
    exitOnSignals(webServer)
  • Run the server:

    $ deno run demo/sample2.ts
    55:933 [Info    ] Logging session initialized. Initial logger min log level: Debug (programmatically set)
    55:933 [Info    ] Create API server
    55:933 [Info    ] Register 'handler' with route GET /
    55:934 [Info    ] Start server
    βœ… Granted env access to "PORT".
    βœ… Granted net access to "0.0.0.0:8000".
    58:410 [Info    ] Apply server request handler 'handler'
    58:410 [Info    ] Apply server request handler 'defaultNotFoundRequestHandler'
    58:410 [Info    ] Accept connection
    58:410 [Info    ] Waiting for new connection
    58:410 [Info    ] Web server running. Access it at: http://localhost:8000/
    58:410 [Debug   ] Handling signal SIGINT
    58:410 [Debug   ] Handling signal SIGTERM
    58:411 [Debug   ] Handling signal SIGQUIT
    58:411 [Info    ] Type 'kill -s SIGINT 35054' to stop
    $ β–ˆ
  • Stop the server:

    $ kill -s SIGINT 35054
    $ β–ˆ

    Server logs:

    57:475 [Warn    ] Received signal SIGINT
    57:475 [Info    ] Stop server
    57:476 [Warn    ] Listener has been closed
    57:476 [Info    ] End processing connection
    57:476 [Info    ] Logging session complete.  Duration: 61543ms
    $ β–ˆ

    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 Response.json({ 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:

    09 09:37:53:001 [Info    ] Handle connection
    09 09:37:53:001 [Info    ] Waiting for new request in connection#8
    09 09:37:53:002 [Info    ] Waiting for new connection
    09 09:37:53:002 [Info    ] Handle request
    09 09:37:53:003 [Debug   ] Request pathname '/execute/stop' matches '/execute/:cmd': apply request handler 'handler'
    09 09:37:53:003 [Info    ] Waiting for new request in connection#8
    09 09:37:53:006 [Debug   ] No more request pending for connection#8
    09 09:37:53:006 [Info    ] End processing request in connection#8
    09 09:37:54:005 [Info    ] Stop server
    09 09:37:54:005 [Warn    ] Listener has been closed
    09 09:37:54:005 [Info    ] End processing connection
    09 09:37:54:006 [Info    ] Logging session complete.  Duration: 5337ms
    $ β–ˆ

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.register({
      path: '/oops',
      handler: () => {
        throw new Error('oops')
      },
    })
    webServer.setNotFoundHandler((req) =>
      Response.json(
        {
          code: 'NOT_FOUND',
          message: `Resource '${req.method} ${req.url}' not found.`,
        },
        { status: 404 },
      ),
    )
    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 },
      )
    })
    
    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."
    }
    
    $ β–ˆ

    Server logs:

    09 09:37:53:001 [Info    ] Handle connection
    09 09:37:53:001 [Info    ] Waiting for new request in connection#8
    09 09:37:53:002 [Info    ] Waiting for new connection
    09 09:37:53:002 [Info    ] Handle request
    09 09:37:53:003 [Debug   ] Request pathname '/execute/stop' matches '/execute/:cmd': apply request handler 'handler'
    09 09:37:53:003 [Info    ] Waiting for new request in connection#8
    09 09:37:53:006 [Debug   ] No more request pending for connection#8
    09 09:37:53:006 [Info    ] End processing request in connection#8
    09 09:37:54:005 [Info    ] Stop server
    09 09:37:54:005 [Warn    ] Listener has been closed
    09 09:37:54:005 [Info    ] End processing connection
    09 09:37:54:006 [Info    ] Logging session complete.  Duration: 5337ms
    $ β–ˆ

    Or in a simpler all-in-one form:

    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 },
        ),
      requestHandlerSpecs: [
        {
          path: '/oops',
          handler: () => {
            throw new Error('oops')
          },
        },
      ],
    }).start()

License

The MIT License