Show HN: Citizen – A Node.js web app framework for fans of classic app servers

github.com

59 points by jaysylvester 10 hours ago

Citizen is a web application framework I started building years ago when I was first learning Node. I've added features and improved stability/performance continuously and finally decided it was worthy of 1.0 status.

I think it might appeal to devs like me (old guys) who came up on app servers like ColdFusion, .NET, PHP, etc. and prefer the server do the heavy lifting. It doesn't dictate anything about your front end, and instead tries to be as flexible as possible in providing whatever output the client might need, whether it's a single fully-rendered HTML view, partials, JSON, or plain text.

Only 2 direct dependencies for terminal commands (commander) and file watching (chokidar). Built-in functionality includes zero-configuration server-side routing, session management, key/value store (cache rendered views, controller actions, objects, and static files), simple directives for managing cookies, sessions, redirects, and caches, and more.

It's been in continuous use on at least one site that receives hundreds of thousands of page views per month, running months at a time without the process crashing. Fairly stable.

Appreciate any comments/feedback/contributions.

danpalmer 6 hours ago

I grew up on Django and have separately seen and contributed to numerous Flask codebases. It has become abundantly clear to me that web apps of any real size that use the library approach (rather than framework) typically end up with a poorly documented internal-only framework anyway, and that plugging together libraries has a significant cost. In my experience this is significantly more of an issue in the JS/TS/Node/etc ecosystem.

My rule of thumb is to look at the feature list for a web framework (Django, Rails, Citizen, etc) – sessions, caching, ORM, rendering, routing, auth, admin, static files, APIs, serialisation, etc etc – and if you need 3 or more, just use a framework that has them all properly integrated.

Example, you're sticking an internal API on your ML model, you need one or two endpoints, and to return some JSON. Just use Flask/FastAPI or whatever the equivalent in your ecosystem is.

Alternatively, you're building a user-facing website, you need routing to endpoints, auth, sessions, and a database. Just use a big framework. You'll likely add more of those cross cutting concerns in the future anyway, and having them all work together rather than gluing libraries together will save a ton of time.

Citizen is the first convincing framework I've seen for this approach from the Node ecosystem. Congrats on the 1.0!

  • WD-42 2 hours ago

    Hard agree. I’ve been going by the 3 feature rule as well. I’ve seen so many flask projects that end up being poorly integrated django project equivalents. Seems that the entire node ecosystem is the same, until now I guess?

Commander_lol 3 hours ago

Congrats on launching your 1.0!

As someone else that is doing something with a remarkably similar sales pitch for the same sorts of reasons, it's interesting to see a very different approach to the one that I've taken.

Are there any particular use cases that you've encountered that wouldn't be a particularly good fit for Citizen? (i.e. do you have an anti-pitch?)

It would be interesting to know what considerations you've made for a few other points I might expect from an app server:

- What's the story around scaling the number of instances of a Citizen app beyond 1? There are a few things like sessions or cache that are stored in globals

- Similarly, how configurable are things like (e.g.) session storage? If, for example, I wanted to use cookies or store sessions in my database

- Is there any prescriptive way that you're supposed to interact with external data? By this I mean, you've got some file structure for storing models, but it's unclear what the expected usage is

I'm looking forward to seeing how this evolves!

  • jaysylvester an hour ago

    As you've noted, citizen is clearly written to handle pretty much everything within the application server itself, which stems from my own particular use cases.

    I suppose that's my anti-pitch: citizen caters to apps that can run comfortably on a single instance and accommodate storage within that instance, or have a web server in front that can distribute clients across multiple app servers and keep each client on the same instance so as to preserve user sessions across requests.

    Now that I consider it feature-rich enough for general use by other devs, I'm thinking about how it could be expanded to accommodate external storage solutions and make the app server stateless.

    I opened this months ago and it directly addresses your point:

    https://github.com/jaysylvester/citizen/issues/120

    The same question applies to citizen's built-in caching, but if I uncouple that from the app server, I have to ask why the dev wouldn't just use a third-party caching solution instead.

    As for external data, I deliberately left models wide open and un-opinionated. They're modules you can put anything into. For my own apps, I use node-postgres, and my models contain all my SQL queries and interact directly with the database. citizen doesn't interact with the model in any way besides storing the module in the global scope so it can be retrieved without importing it (which I do to support hot module reloading).

    Curious to see what you've been working on and your approach.

replwoacause 2 hours ago

Good job!! Can’t wait to give it a go. Since you started work on this 10 years ago, has anything like it emerged in the Node ecosystem? What is most similar framework to Citizen right now and what makes it different?

  • jaysylvester an hour ago

    I don't know what's out there that's similar to citizen, honestly. I didn't set out to create competition for existing frameworks; I just built the stuff I found useful for my own projects and think it's finally at a point where others might find it useful also.

    Basically, I wasn't a fan of what was available 10 years ago, so I built what I needed and didn't pay much attention to what others were doing.

    One of my fears in sharing it widely was getting a response like, "You're about 5 years late, frameworks X, Y, and Z do all these things already." But based on some of the comments, it seems like citizen offers something that others don't.

    What I like about citizen is that it provides enough structure to get started, keep my code organized, and focus on what I'm building as opposed to how, while remaining flexible and hands-on enough for me to feel like I'm actually writing code and not just gluing together everybody else's code.

    • replwoacause 37 minutes ago

      Well I’m glad you chose to share it. It looks awesome and I’ll be giving it a whirl for some side project work this weekend. Thank you!

williamdclt 9 hours ago

Congrats! Seems like a considerable project, I appreciate the low-dependency approach.

> hundreds of thousands of page views per month

I’m not sure I’d include that as any sort of perf/stability argument. Even only assuming traffic is only clustered over 8h of the day, it’s 0.1 to 1req/sec. The worst web framework out there in any language probably could handle that

  • jaysylvester 8 hours ago

    That's a fair observation. I only include it because the Node way of doing things seems to be "just let the process crash and restart it", and that's been a tough concept for me to accept. I'm particularly proud of error handling in citizen and want it to be as much like an old-school app server as possible.

    I still use pm2 on the aforementioned site just in case though, and maybe seeing that extended process uptime is just a feel-good exercise, but still worth mentioning.

    I've been meaning to do real performance testing and keep pushing it off because it's been adequate for my needs.

    • pier25 4 hours ago

      > the Node way of doing things seems to be "just let the process crash and restart it"

      Honestly I've never heard of anyone advocating for this.

      PM2 is only necessary if you're running Node in like a VPS where you're in charge of managing the process. In managed solutions like Fly, Google Cloud Run, etc you never really have to care about this. VMs are restarted automatically and it's trivial to have HA with multiple VMs running in parallel.

      • jaysylvester 4 hours ago

        The scenario you described is exactly how I host my sites now (Debian on DO).

        I've seen numerous engineers advocate for letting the app crash (or at least gracefully shutting it down), capturing the error, and restarting. Errors crash the Node process by design after all. Perhaps it's not as prevalent as it used to be, but it was certainly considered acceptable and even desirable for a time to let the app crash and restart it with pm2, forever, etc. since an error could leave the app in an unknown state.

        citizen has a config option for keeping the app running or exiting the process in the event of an error, so it's up to the dev.

accrual 8 hours ago

This is really cool! I cut my web development teeth on LAMP (Linux Apache MySQL PHP) and MVC was my favorite pattern to implement. I appreciate how it breaks out each concern into its own folder or files and gives the developer control over the details like session management. For small projects I still find it fun and comfortable to write web apps this way.

  • jaysylvester 5 hours ago

    Thanks!

    "Fun" and "comfortable" haven't applied to web dev for a long time in my opinion, and my preference is to keep it as simple and enjoyable as possible.

usbsea 7 hours ago

What does "server do the heavy lifting" mean here - and is this much different to a Rails or Django type of thing? I know in Node-land there are less kitchen-sink options like this though.

The storage and k/v looks interesting. Django is pretty pluggable in that regard. Choose sqlite and a local redis and I think you probably have the same thing. But you do need to make choices and read docs to get there.

  • jaysylvester 6 hours ago

    It mostly means keeping routing/rendering/etc. on the server rather than client. My preference has always been to let the server render the initial request in full (feed the client an entire HTML document) and then update on the client side as necessary (which could be data via JSON or chunks of HTML, whichever makes sense) with progressive enhancement and minimal JS as the goal.

    You can of course build an API with citizen that only returns JSON responses and do all the rendering on the client if you like. It's meant to be flexible that way. Basically, the client-side stuff is completely up to you. I've toyed with the idea of building a client-side companion library that wires everything up so server citizen and client citizen talk to each other and do what I described in the previous paragraph automagically.

    I started citizen a decade ago, so many options available today were either non-existent or in their infancy then. I haven't performed a competitive analysis to compare features with other frameworks. You nailed it though on the lack of kitchen-sink-type frameworks in Node (especially 10 years ago), which is why I built citizen. And I didn't like the idea of depending on so many third-party packages.

mannyv 9 hours ago

Your docs are great!

  • jaysylvester 9 hours ago

    Thank you! I try to be thorough. Getting to be a bit much for a readme though, probably deserves its own site.

korkybuchek 8 hours ago

Are there any tests for this library? And it looks like you don't support Typescript, is that planned?