Routing

The @stricjs/router component

Install the router and utilities:

bun add @stricjs/router @stricjs/utils

The router component provides the routing part for Stric, like Express or Fastify.

The utilities component has helpful utilities to parse and handle requests.

Define routes

You can define a route using [method](path, handler), for instance:

import { Router } from '@stricjs/router';  

export default new Router()
    .get('/', () => new Response('Hi'));

In the above example, we create a new router and this router will respond with Hi on every GET request to pathname /. The router can be served with export default or Bun.serve directly because it is a serve options object.

All method handlers

To add a handler for all methods, use app.all with the same parameter as above.

import { Router } from '@stricjs/router';

export default new Router()
    .all('/', () => new Response('Hi'));

Request methods

You can define a route with another request method as well.

import { Router } from '@stricjs/router';  

export default new Router()
    // You should use new Response(JSON.stringify(...), { headers: { ... } })
    // for it to be faster. This example provides a clean way to do this
    .post('/json', () => Response.json({ hi: 'there' }));

Compares to the first example, this example add another handler which will run on every POST request to pathname /json. This handler will return an object as a response.

Parametric routes

You can parametric routes to retrieve data from URLs, works like Express and Fastify routes.

The type of req.params is inferred directly based on the path.

import { Router } from '@stricjs/router';  

export default new Router()
    .get('/id/:id', req => new Response(req.params.id));    

In this example, the server will respond to every route that matches /id/:id with the specified id parameter value in the URL. For example, if we send a GET request to pathname /id/90 we will get 90 as a response.

Wildcard

You can use the wildcard * to match the rest of the path.

import { Router } from '@stricjs/router';  

export default new Router()
    .get('/private/*', req => 
        new Response(`Cannot access /private/${req.params['*']}`)
    );

This will respond to every request pathname starts with /private/.

Body parsing

Stric has a set of predefined body parser you can use.

import { Router } from '@stricjs/router';

export default new Router()
    .post('/text', req => {
        // Use the parsed body here (it has type hint)
        req.data;
    }, { body: 'text' });
    
// More parsers:
{ body: 'json' } // Parse to JSON
{ body: 'blob' } // Parse to Blob
{ body: 'form' } // Parse to FormData
{ body: 'buffer' } // Parse to ArrayBuffer
{ body: 'none' } // Don't parse body. This is the default value

Query parsing

The req.query parameter returns the query start index, include the ? character. You can parse query with @stricjs/utils:

import { Router } from '@stricjs/router';  
import { query as parse } from '@stricjs/utils';

export default new Router()
    .get('/id/:id', req => {
        // If the query does not exist only respond with the ID
        if (req.query === -1) return new Response(req.params.id);

        // Get the 'name' parameter from query if query exists
        return new Response(
            req.params.id + ' ' + parse(
                // Get the query string without '?'
                req.url.substring(req.query + 1)
            ).name
        );
    });

Guarding

Guarding routes and validating data.

Routes

Guard routes work like wildcard but these routes are invoked first to check whether a specific sub-route should be handled.

import { Router } from '@stricjs/router';

export default new Router()
    // Return null to tell the router to use the 404 handler
    .guard('/', req => req.path === 'forbidden' ? null : true)
    .get('/', () => new Response('Hi'))
    .post('/json', req => req.json().then(Response.json))
    .get('/forbidden', () => new Response('Never response'));

Input validations

The @stricjs/utils component provides a simple and fast guard for checking user input.

import { Router } from '@stricjs/router';
import { guard } from '@stricjs/utils';

const check = guard.create({ 
    name: 'str', // string
    age: 'num', // number
    address: '?str' // Optional string (prefix with ?)
    /*
        More things here, for example:
        data: {
            isRegistered: 'bool'
        }
    */
});

// Return the current object if the type matches, else null
// For this case it matches so it returns the passed object
check({
    name: 'Dave',
    age: 23
});

// Register a new type
guard.register('mycustomtype', (object: any) => {
    // Do validation here, should return true or false
});

/*
    Basic types:
    - 'str': string
    - 'num': number
    - 'bool': boolean
    - 'nil': null
    - 'undef': undefined
    - 'bf': Buffer
*/ 

Right now guard function is limited and does not support arrays (Arrays are very hard to compose).

Router groups

Using groups to split the route handlers.

import { Router, Group } from '@stricjs/router';

// IDs
const idList = {
    '0': 'a',
    '1': 'b',
    '2': 'c'
};

// Response stuff
const notFound = { status: 404 };
const msg = 'Welcome! Go to /search/:id to search for specific IDs';
const group = new Group('/search')
    .get('/:id', req => {
        if (req.params.id in idList)
            return new Response(idList[req.params.id]);
        return new Response('Item not found', notFound);
    });

// Router
export default new Router()
    .plug(group)
    .get('/', () => new Response(msg));

This is a simple example which create a new group with the root set to /search, so the actual path that is registered is /search/:id, not :/id.

Special handlers

Stric provides a fast way to handle 404 when a route handler is not found.

import { Router } from '@stricjs/router';

export default new Router()
    .get('/', () => new Response('Hi'))
    .use(404, req => { /* Maybe something here to return */ });

Call use(404) to use the default 404 handler, use(404, handler) to add a custom handler.

For error handling, you can use the default 500 handler or register a custom handler the same way as the 404 handler.

import { Router } from '@stricjs/router';

export default new Router()
    .get('/', () => {
        throw new Error('Error');
    })  
    .use(500, err => new Response(err.message));

Storing states

You can store states and methods for using in requests.

import { Router } from '@stricjs/router';

export default new Router()
    .store('count', 0)
    // On every request increment count by one and return it as a response
    .get('/increment', (_, store) => new Response(
        String(++store.count)
    ));

All states are stored in an object which is passed as a reference to the handler as the second argument, which mean you can change the properties of the store but not the store itself.

WebSocket

You can split routes for WebSocket handlers.

import { Router } from '@stricjs/router';

export default new Router()
    .ws('/', {
        message(ws) {
            ws.send('Hi client!');
        }
    });

WebSocket routes cannot collide with other route handlers of the router, but you can still register route handlers with the same pathname but not GET method.

Details
  • WebSocket handlers are stored in a list, which is then attached with the upgrade data when server.upgrade(req, opts) is called.

  • The WebSocket handler is stored in the static route handlers list as a number which represents the index of the handler in the WebSocket handlers list.

  • When the path matches, the WebSocket handler is attached with the current Request object as a property _. The request object will be sent to the WebSocket event handlers as attached data.

  • Each WebSocket event handler will check if the specific handler for that event was registered. If it is registered the event handler will be executed.

Optimizations

Here are some optimizations you can do to make your Stric application faster.

Set the base URL

You can specify the exact base URL of your app to make path parsing faster. Note that will not works correctly if your app has subdomains.

Details
  • The createFetch function calculates the length of the base property.

  • The calculated length actually is the start index of the path part, so we can inject it directly into the code to check for query start index faster.

import { Router } from '@stricjs/router';

export default new Router({ base: 'http://localhost:3000' })
   .get('/', () => new Response('Hi')); 

No path parsing

Set parsePath to false will skip slicing the path for matching routes and use the URL directly. This optimization will only work with the base optimization.

import { Router } from '@stricjs/router';

export default new Router({ 
   base: 'http://localhost:3000', 
   parsePath: false 
}).get('/', () => new Response('Hi')); 
Details
  • Normally req.path is parsed to check the pathname.

  • If you disable path parsing req.path will not be usable.

Faster query parsing

If you only need the value of a single key in the query, you can create a specific parser with @stricjs/utils to search for the value. This function will return the value without decoding.

import { Router } from '@stricjs/router';
import { qs } from '@stricjs/utils';

// Only search for a single value of name
const parse = qs.searchKey('name');

export default new Router()
    .get('/', req => new Response(
        // Get the value of the name parameter if presented
        // If not presented this function will return null
        parse(req.url, req.query + 1)
    ));

Using macros

Macros inject your code directly to your fetch without caching and calling the handler like usual, which can reduce response time. This feature should only be used for small handlers.

import { Router, macro } from '@stricjs/router';

export default new Router()
    .get('/', macro(() => new Response('Hi')));

For returning a string response, macro has a shorthand.

import { Router, macro } from '@stricjs/router';

export default new Router().get('/', macro('Hi'));

Limitations

Because macro code is injected directly to the fetch function, it has some limitations.

  • Macros cannot access outside variables. To access variables outside the function scope you need to store it using app.store().

  • Macros code cannot use await. The fetch function is synchronous. You can return a promise, but can't use await.

  • Macros need proper parameters name. Code in macros that directly use the request and store need to have the same name as the variables in fetch.

Last updated