Best low-magic web frameworks in 2023?

I’m interested in low-magic (little opinionated, minimalistic) web frameworks. They give me a way to respond to endpoint requests with ByteStrings, and little/nothing else.

  1. The ones I know about are Snap, Scotty, Spock. Are there any others?
  2. Which are the most maintained these days? Are they (in 2023) suitable for heavy-duty production software?
3 Likes

These days I recommend Twain.

2 Likes

See also the recent blog post by @gilmi: λm.me - Why I use the Twain web framework

4 Likes

I really like the look of Twain, but am a bit worried at the routing performance. From what I can tell, its basically brute force matching over all routes, rather than being a bit more intelligent and building a trie. It’s certainly simple, but feels unfortunately wasteful. I’d love something that looked like twain but had a tiny bit more routing smarts. Maybe that can just be layered on top though

2 Likes

Perhaps Twain itself could be modified to be more performant?

I don’t know if a trie is the best way to approach this. It’s certainly ideal when you have a lot of paths, but for a small app, you can probably get away with something like

grouped :: (Request -> Maybe Request) -> (Request -> Request) -> [Middleware] -> Middleware
grouped match restore ms app req = case match req of
  Nothing -> app req
  Just req' -> foldr ($) ms (\req'' -> app (restore req'')) req'

type RoutePrefix = ...
stripPrefix :: RoutePrefix -> Request -> Maybe Request
stripPrefix = ...
restorePrefix :: RoutePrefix -> Request -> Request
restorePrefix = ...
prefixed :: RoutePrefix -> [Middleware] -> Middleware
prefixed prefix = grouped (stripPrefix prefix) (restorePrefix prefix)

main :: IO ()
main = do
  run 8080 $
    foldr ($) (notFound missing) routes

routes :: [Middleware]
routes =
  [ get "/" index
  , post "/echo/:name" echoName
  , prefixed "/foo"
    [ get "/" fooIndex
    , post "/echo/:name" fooEcho
    , get "/bar" fooBar
    ]
  ]

I think if you do anything much more intelligent, you lose the simplicity of everything being a Middleware.

1 Like

Couldn’t the run function merge all the routing middlewares into a trie under the hood? Then I think you can just keep the same interface that Twain already has.

The run function isn’t a part of Twain. In this case (because I’ve just pasted part of the example in the readme with some changes), it’s from Warp, but it could be from anything that can serve a WAI application. The Middleware type used is also not from Twain, it’s from WAI, and as a consequence there is no way to see if a Middleware “matches” a given route. Twain is a very thin layer on top of WAI.

1 Like

How can I write HTML/CSS/JS using Haskell syntax but with solid caching support (fast loading websites)?

I’ve heard of Blaze, Clay, and Fay - but can these do caching?

It looks like Heist/Snap has caching (for HTML) but this is not Haskell syntax.

I use lucid2 for writing html. I don’t use an eDSL for CSS & JS because I often times have to compare code snippets and doing the translation in my head is harder. Moreover this prevents fluid collaboration with UI experts.

2 Likes

Why is there a separate lucid2 package on Hackage?

What about caching (as Snap/Heist has), performance differences? TBH maybe I don’t need any caching. If someone can tell me lucid is just as fast as any HTML output from a Node.js program, I’m fine.

Why is there a separate lucid2 package on Hackage?

Separate from what? lucid2 generates HTML.

What do you mean when you say “as fast as any HTML output from a Node.js program”? There are a lot of ways to generate html from Node, each with different performance characteristics, and the same is true for Haskell.

If you mean why separate from lucid (1), then it’s because the author believes in the Immutable Publishing Policy (which is an idea with a lot of merit in my opinion).

1 Like

Let me put it like this, are there any reasons to believe Heist/Snap is (because of its caching) faster than lucid2?

@mightybyte feel free to chime in if you can.

There are absolutely things you could do, but at this point I don’t really think it’s Twain anymore. I think that’s fine, Twain doesn’t really need to change, it’s just a fairly fundamental limitation of this type of chaining-middleware framework. For some applications I’m sure that’s absolutely fine.

1 Like

Re routing: A path-element based trie is a pretty obvious optimization, and shouldn’t be difficult to implement. You would just need a “helper” middleware that takes a dictionary of “subdirectories” to sub-handlers, inspect the request to pop off the first path element, look up the matching sub-handler (and serve a 404 if it doesn’t exist), and delegate to that sub-handler. And then you can nest those dispatchers for whatever tree depth your routes need. I’ve done this against plain WAI before, and it works fine. The trickiest part is deciding how you do the “popping” - you can either actually modify the request, so that for each nested sub-handler, the first element in the path it processes is the one that it cares about; the advantage is that the sub-handler is blissfully unaware of the part of the route that it doesn’t need to care about, but the downside is that if you’re serving something that needs to link to other resources, then absolute links you generate based on the “current path” as per the HTTP request will be wrong. Alternatively, you can add the handler’s local path to its configuration, and make it skip that prefix when processing a request - downside of that is that it makes the setup a bit awkward.

Re caching: you can just make a caching middleware and run your WAI application behind it for full-response caching, and this should give you the same kind of performance that you’d get from any other web framework’s full-response caching. IIRC, there are ready-to-use packages on hackage for that, but don’t quote me on that. If you want to cache fragments, then you will need a solution specific to whatever HTML rendering system you use anyway, but whatever you are using, integrating it with WAI (or Twain, or Scotty, or whatever you end up using) should be fairly straightforward.

2 Likes

Sorry for the late bump, but just to verify:

@Kleidukos @gilmi, etc.: Given the posts that came later into this thread, about Twain’s potential performance issues with routing, and (what looked to me like) “hacks” that are currently needed to fix them, would you still recommend Twain?

Maybe these performance issues won’t be a problem unless for very large (e.g. thousands of?) routes? Please share more details if you have time, @ocharles etc.

Chiming in as a current scotty maintainer: do give it a try, scotty: Haskell web framework inspired by Ruby's Sinatra, using WAI and Warp is the latest and greatest.

While we’re at it, while I hate creating additional work to people, @gilmi your review of scotty is great but only applies to versions < 0.20 ^^

3 Likes