DaloyJS in 2027: The TypeScript REST API Framework Built for the Vibe-Coding Apocalypse and Alternative to Express?
Vibe-coded apps are getting breached because nobody reads the code anymore. This is the blunt case for DaloyJS, the secure-by-default TypeScript REST framework I now use instead of Express, with a migration guide for the move.
I have been writing backends for about ten years now. I started in Manila and now live in Norway. Most incidents I have cleaned up came from code nobody really read. In 2027 we call that vibe coding.
Vibe coding started as a joke. Deployed code made it a security concern.
The state of the world in 2027
Most backend code today is written by an AI. I am not being dramatic. Non-developers describe an app in plain English, the model produces something that runs, and it gets deployed within the hour. Engineers are doing the same thing, just with more confidence and slightly better commit messages. We let agents install dependencies, run the tests, and open the pull request. The code works on the happy path, the demo looks great, everyone claps.
Then it goes to production with no body limits, input validation that is "optional" because it slowed down the demo, an admin route that somebody forgot to unmount, and an outbound fetch that will cheerfully call http://169.254.169.254/latest/meta-data/ if you ask it nicely. The app is one crafted request away from leaking its own cloud credentials, and nobody on the team can tell you that, because nobody on the team wrote that line.
The model followed the prompt exactly: "make it work." Security requirements were absent. A line from the Supabase and Aikido write-up on secure-by-default development that I keep pinned to my monitor: "If you tell an AI to make something work, it might remove the very security checks that protect you." That is the whole problem in one sentence. The model optimizes for the request, and "make it work" almost never includes "and also do not let strangers read my database."
On top of that, the dependency tree itself became the attack surface. In the last couple of years we have watched self-replicating npm worms, malicious postinstall scripts, CI cache poisoning, and a newer favorite called slopsquatting, where an attacker pre-registers a package name that an AI assistant is statistically likely to hallucinate. Your agent confidently runs pnpm add @types/fastify-helmet, the package exists because somebody was waiting for exactly that hallucination, and now you have a wallet drainer in your lockfile. Fun.
That is the environment DaloyJS was built for: code produced quickly by machines, deployed by people who trust the output, on top of a registry that has become a minefield.
What DaloyJS actually is
DaloyJS (@daloyjs/core) is a runtime-portable, contract-first TypeScript web framework. That is a mouthful, so here is the plain version. You define a route once. That single definition is the source of truth for validation, your TypeScript types, your OpenAPI 3.1 document, your typed client, and your contract tests. Routes stay readable without decorators, while the shared schema keeps the implementation and docs synchronized.
And it runs the same code on Node, Bun, Deno, Cloudflare Workers, Vercel (Node and Edge), Fastly Compute, and AWS Lambda. The core only ever sees a web-standard Request and returns a Response. The runtime-specific stuff lives in thin adapters at the edge.
But the part I actually care about, the part that made me stop using other frameworks for new projects, is the design stance. DaloyJS attacks the vibe-coding problem from two directions at the same time.
- A secure-by-default runtime. The dangerous things are off until you turn them on. The safe things are on until you turn them off. The framework will literally refuse to boot on certain unsafe configurations.
- A hardened supply chain. Zero runtime dependencies, npm provenance, SBOMs, and a pnpm posture that assumes the registry is hostile, because it is.
The secure path keeps the same developer experience. Guardrails ship enabled, and disabling them takes an explicit change. The smallest app shows how that works.
Hello world with production guardrails
That complete API already has a hard body-size cap so a 4GB upload cannot eat your memory, a request timeout so a slow-loris client cannot hold your handlers hostage, a JSON parser that strips __proto__ and constructor so prototype pollution does not happen, a router that rejects .. path segments before it walks anything, header sanitizers that reject CRLF and NUL so nobody can smuggle a response split through your logs, and RFC 9457 problem+json errors that redact their detail field in production so your 500 does not hand a stranger a stack trace.
The validator is Zod 4 here, but DaloyJS speaks Standard Schema, so Valibot, ArkType, and TypeBox all work too. No lock-in. If your team already has opinions about schema libraries, bring them.
Now compare that to a typical vibe-coded Express app. The model gives you app.get('/books/:id', ...), it works, and it ships without a body limit or timeout. The JSON parser will happily accept {"__proto__": {"isAdmin": true}}. The error handler prints the stack trace because that was helpful during development and nobody removed it. None of that is malicious. It is just absence. DaloyJS is built on the idea that absence is the bug.
The framework that refuses to start
This is my favorite feature and it sounds aggressive when you first hear it. DaloyJS refuses to boot on known-dangerous configuration. It throws instead of logging a warning and continuing with an unsafe process.
Things that will stop your app from starting:
cors({ origin: "*" })withcredentials: true. This is the classic "I just wanted the CORS error to go away" move, and it is also how you let any website on the internet make authenticated requests as your logged-in user. DaloyJS throws at construction.- A weak session secret. If your secret is shorter than 32 bytes or is one of the known weak strings (think
"secret","changeme"), it will not boot. - A
session()plus a state-changing route with nocsrf()protection. The framework notices you have cookies and a POST route and no CSRF, and it stops you. - Unconfigured
X-Forwarded-*handling in production. If you are behind a proxy and you have not told the framework whom to trust, it will not silently believe spoofed client IPs.
I know some of you just twitched. "What if I have a legitimate reason?" Then you fix the default for everyone with a scoped knob, you do not strip the guardrail inline. The framework's philosophy, which is written into its contributor rules in capital letters, is that bad defaults are bugs. If a guard genuinely blocks a real use case, the answer is a narrower override, not a deleted check. For service-to-service deployments behind a mesh there is even a preset: "internal-service" that turns off the browser-only guards (the CORS guard, the CSRF boot guard, auto secure headers) while keeping every input, parser, credential, and SSRF guard on. The choice gets logged at boot, and you can audit the live posture with app.getSecurityPosture(). Opt-in, visible, reversible. That is the pattern.
AI produces plausible configuration. A refuse-to-boot guard converts that plausibility into a CI failure before it can become a production incident.
SSRF, or the call your handler should never make
A common SSRF path starts when an app accepts a URL from a user, perhaps for a webhook, an image import, or a link preview. The handler does await fetch(url). An attacker passes http://169.254.169.254/latest/meta-data/iam/security-credentials/, your server fetches it, and now the response body contains your instance's IAM credentials. This is Server-Side Request Forgery and it is responsible for some of the biggest cloud breaches we have on record.
DaloyJS ships fetchGuard(), which is a drop-in replacement for fetch with a hard-deny floor.
The deny list covers every documented cloud metadata IP: the AWS, Azure, DigitalOcean, and GCP IMDS address 169.254.169.254, the AWS ECS task metadata and EKS Pod Identity ranges, the Oracle 192.0.0.192, the Alibaba 100.100.100.200, link-local, loopback, and private ranges. Redirects trigger a fresh resolution, so an attacker cannot hand you a friendly https://example.com that then 302s to http://169.254.169.254. The hard-deny floor cannot be lifted by any allow flag. Even if you misconfigure your allow list, you cannot accidentally re-expose the metadata endpoint. Secure defaults put this hard-deny floor in place before application configuration.
Tokens, because everybody gets JWT wrong
JWT is a minefield and most tutorials walk you straight into it. The alg: "none" attack, algorithm confusion where an attacker swaps RS256 for HS256 and signs with your public key, tokens with no expiry that live forever. DaloyJS bakes the lessons in.
The signer and verifier refuse alg: "none". They accept only an explicit algorithm allowlist, never "whatever the token claims." They refuse to mix HS secrets with JWK key material, which is the algorithm-confusion defense. They refuse to sign a token without an exp. And they refuse HS-shaped secrets under 32 bytes per RFC 7518. The jwk() middleware is asymmetric-only on purpose, requires https:// JWKS URLs, caches them with in-flight promise dedup, and cross-checks the kid and the JWT-versus-JWK algorithm. You do not have to know any of this to be protected by it, which is the point. The default is the secure one.
DaloyJS is a resource server, by the way, not an identity provider. It verifies and enforces tokens. It does not run a login page. Bring Keycloak, Zitadel, Auth0, Entra ID, Cognito, whatever you like. Do not build your own authorization server. I have seen people try. It does not end well.
Now the developer experience, which is why you will actually use it
Security tools fail when teams hate using them. People route around friction, and a disabled guardrail creates false confidence. DaloyJS therefore has to be pleasant in ordinary work. The typed-client flow below is what convinced me.
One route definition gives you a typed client with zero codegen:
That getBookById method, its input shape, and its per-status response union are all inferred from the route definition. If you change the route, the client type changes, and your consuming code stops compiling until you fix it. The client stays synchronized without a generation step or a stale-contract meeting. For consumers that cannot import your TypeScript (a different repo, a different language), one pnpm gen command runs your live OpenAPI spec through Hey API and emits a fully typed fetch SDK.
Interactive API docs take one line:
That mounts an interactive Scalar UI at /docs, plus the JSON and YAML specs, with a strict CSP. Switch to Swagger UI or Redoc with one word (ui: "swagger"). The docs are always contract-accurate because they are generated from the same route definitions that run your server. They cannot go stale. If you omit the info block, DaloyJS reads your package.json for the title and version. Less boilerplate, fewer things to forget.
A fuller route includes auth, validation, and typed error responses:
The bearerAuth comparison uses a timing-safe equal under the hood, so you do not leak token bytes through response timing. The body is validated against the schema before your handler runs, and if it fails, the client gets a clean 422 problem+json, not a 500 with a stack trace. You declared 422 in the contract, so it is in your OpenAPI doc and your typed client too. The route definition remains the source for each layer.
The supply chain, because your dependencies are also your attack surface
You can write the most careful handler in the world and still get owned through a postinstall script in a transitive dependency you never chose. So the second front matters as much as the first.
@daloyjs/core has zero runtime dependencies. Not "few." Zero. That is enforced in CI by a gate called verify:no-runtime-deps, and it is treated as a floor, not a goal. A hallucinated dependency literally cannot transitively land in the published tarball, because there is no dependency tree to hide in. Every feature I described in this article, the JWT verifier, the SSRF guard, the WAF, the rate limiter, the WebSocket layer, is built on web-standard APIs and Node built-ins. No node_modules surprise party.
The scaffolder ships a hardened .npmrc:
ignore-scripts=true kills lifecycle-script payloads, which is how most install-time worms detonate. minimum-release-age=1440 refuses to install anything published in the last 24 hours, which is the typical window in which a malicious package gets detected and unpublished. Combined, those two defaults blunt slopsquatting hard. The attacker pre-registers the name your agent will hallucinate, but your install refuses fresh packages and refuses lifecycle scripts, and a workspace gate called verify:known-dep-names refuses any top-level dependency name that is not on an explicit allowlist. So pnpm add some-hallucinated-name cannot slip into a package.json. It forces a one-line diff that a human has to look at. That review checkpoint is the whole defense, and the framework makes it unavoidable.
On top of that, @daloyjs/core is published with npm provenance and ships CycloneDX 1.5 plus SPDX 2.3 SBOMs, regenerated and verified on every release. There is a whole pile of CI gates with names like verify:no-registry-exfiltration, verify:no-remote-exec, verify:no-lifecycle-scripts, and verify:no-weak-random, and they carry IOC coverage for active campaigns. There are even gates that scan AI-agent config files (CLAUDE.md, .cursorrules, AGENTS.md) for the prompt-injection persistence trick the TrapDoor crypto-stealer used, and gates that refuse editor configs that auto-run a command on folder open, which is how the Miasma worm detonated. The supply chain is treated as hostile, which in 2027 is just accurate.
Defense in depth, when the edge WAF is somebody else's budget line
Not every team has a CDN WAF in front of their app. DaloyJS gives you first-party, opt-in layers so the framework itself can do some of that work:
waf()runs a scored inbound inspection pass for SQLi, XSS, NoSQL operator injection, and command injection. It is defense in depth, not a ModSecurity replacement, and it has alogmode so you can tune against real traffic before you start blocking.autoBan()is fail2ban for your app: repeat offenders who keep hitting 401/403/429 get temporarily banned, with exponential escalation and decay.botGuard()verifies declared crawlers with reverse-DNS plus forward-confirm, so a request claiming to be Googlebot has to actually be Googlebot.geoBlock(),ipReputation(),concurrencyLimit(), andrequestDecompression()(a zip-bomb guard that aborts during inflation) round it out.
And on WebSockets, the framework closes the Cross-Site WebSocket Hijacking class of bug by refusing to register a production WS route unless you provide an Origin policy or explicitly acknowledge a cross-origin upgrade. Cookie auth alone does not stop a malicious site from opening an authenticated handshake from your user's browser, which is exactly the Storybook CVE-2026-27148 pattern. The Origin check runs before your upgrade hook, on both Node and Bun. Again: the safe thing is the default, and you have to go out of your way to remove it.
Same code, every runtime
Because the core is just Request to Response, deployment is a one-line import swap. No rewrite when your platform decision changes, and platform decisions always change.
The routing is fast too, if you care about that sort of thing (I do). Static routes resolve through a single Map.get at roughly 12 million ops per second, dynamic routes walk a trie in time proportional to the number of path segments, body parsing is lazy and only runs when a route declares a body schema, and there is no regex on the hot path. The secure defaults do not cost you throughput.
When DaloyJS is a practical Express alternative
The title of this post ends with a question mark on purpose. "Alternative to Express" is a big claim, and Express is mature software. It powered half the internet for a decade, remains in my muscle memory, and carried plenty of code that paid the bills. My argument is narrower: the trait that made Express great in 2015, that it does almost nothing and gets out of your way, is exactly the thing that makes it dangerous in the vibe-coding era of 2027.
Think about what an Express app actually is. In its own documentation it is described as "essentially a series of middleware function calls." You wire up (req, res, next) callbacks, you mutate res, and you call res.send() to end the cycle. That is a beautiful, minimal model. It is also a blank canvas, and a blank canvas is the worst possible thing to hand an AI that was told to "make it work." Unless prompted, the model may omit helmet, body limits, rate limiting, request timeouts, and validation for req.body, which is typed any, so TypeScript will not save you either. Every one of those is something a human has to remember to bolt on, and the entire premise of vibe coding is that nobody is remembering anything.
The migration guide lists the concrete differences between DaloyJS and Express:
- OpenAPI and a typed client for free. In Express you bolt on
swagger-jsdoc, hand-write JSDoc comments above each route, and pray they stay in sync with the code. They never do. In DaloyJS the route definition is the spec, so it cannot drift, and a typed SDK falls out ofpnpm gen. - Validation the type system actually trusts. Express hands you
req.body as anyand wishes you luck. DaloyJS validates with Standard Schema (Zod, Valibot, ArkType) and infers your handler'sparams,query, andbodytypes from the same schemas that do the runtime checking. One source of truth, no casts. - Secure defaults instead of a TODO list. This is the whole thesis of the article. Express ships almost nothing on the security front. DaloyJS ships body limits, request timeouts, header sanitization, prototype-pollution-safe JSON, and one-line
secureHeaders()andrateLimit()helpers, and it refuses to boot on configurations that are known to be unsafe. - Run the same app everywhere. Express is welded to Node's
httpmodule. DaloyJS is built on web-standardRequestandResponse, so the same app object runs on Node, Bun, Deno, Workers, and Lambda. When your platform decision changes (it will), you swap an import, not a framework. - Zero runtime dependencies. A fresh Express app pulls in dozens of transitive packages, and every one of them is a slopsquatting target and a
postinstallrisk.@daloyjs/corehas none. There is no dependency tree for a malicious package to hide in.
There are also times when you should not do this. If your app is mostly server-rendered HTML through a view engine like EJS or Pug, DaloyJS is API-first and will fight you. If you depend on one weird Express middleware with no equivalent and no appetite to port it, check that first. And if the app is in stable maintenance-only mode, migration has a cost and you should spend that energy somewhere with upside. Greenfield services, anything where you were about to add OpenAPI anyway, and apps that keep getting bitten by untyped req.body bugs are the sweet spot.
The migration guide
The docs include a complete, no-prior-knowledge Migrate from Express.js to DaloyJS guide in the docs that maps every Express concept you already know to its DaloyJS equivalent, with before-and-after code for each one.
The core mental shift is one sentence: you stop mutating res and calling next(), and you start declaring a contract and returning a value. Here is the side-by-side that the guide opens with:
The thing I want you to notice is how much code disappears in the translation. Take a typical Express route with manual validation and manual error handling:
The DaloyJS version of that route deletes the body-parser line, the manual if (!id || !title) check, the hand-rolled auth status, and the catch-all error middleware, and gives you an OpenAPI entry and a typed client in exchange:
You throw a NotFoundError instead of building one by hand. You declare the 422 and DaloyJS returns it for you when validation fails, so your handler only ever runs on valid input. Your helmet becomes secureHeaders(), express-rate-limit becomes rateLimit(), csurf becomes csrf(), cookie-parser becomes readRequestCookie() / serializeCookie(), and express.Router() becomes either app.group() or a proper encapsulated plugin. The migration guide has the full mapping table for all of them, so you are not guessing which DaloyJS thing replaces which Express package.
And you do not have to do it in one heroic weekend. The guide documents a strangler-fig approach: stand DaloyJS up next to your existing Express app, put a reverse proxy in front, and move one resource at a time (/books first, then the next), pointing the proxy at the new routes as they land and deleting the old ones. Both apps can share the same database, the same session store, and the same JWT secrets during the transition, so logins keep working no matter which app serves a given request. You lock each move with contract tests using the in-process app.request() client (no port, no second terminal), and you repeat until Express is empty and you can delete it. That is how you migrate a real production app without a scary big-bang cut-over.
If you want the persuasion version rather than the how-to version, there is also a companion post, Why DaloyJS is the best Node.js Express alternative, that makes the case in more detail. The migration guide is the better read, because it shows you the actual code instead of just telling you it is nicer.
Starting a project
You get a working project with the secure middleware stack already wired, docs: true on, a runtime template of your choice, and containers that ship with a non-root user, tini as PID 1, a HEALTHCHECK, and STOPSIGNAL SIGTERM. The --with-ci bundle even signs your pushed images with Sigstore Cosign and attaches an SBOM attestation, so your consumers can verify the image instead of trusting the registry. The scaffolder applies those operational details before the first request ships.
Why I think this is the right bet for 2027
DaloyJS is maturing in public, and its API follows SemVer. It cannot write your business logic or stop every bug. A determined developer can still bypass a framework, and I say that as one who has needed saving.
The way we build software changed. Code volume rose while human review shrank, and the registry turned into a hunting ground. A framework now needs opinions and safe defaults. It should reject the obviously dangerous request even when a developer or an AI asks with confidence. Removing a security control should require an explicit choice.
DaloyJS makes the boring stuff the default and the dangerous stuff the exception, and it does that without making you miserable. One route definition, full types, live docs, a typed client, a hardened runtime, and a supply chain that assumes the worst. If you are starting a new TypeScript API in 2027, that is the trade I would take every single time.
Go read the security docs, run pnpm create daloy@latest, and try to make it boot with a wildcard-credentials CORS config. It will tell you no. That "no" is the whole product.
About the author: Filipino developer in Norway, about ten years of shipping backends, and still convinced that the most dangerous line of code is the one nobody read before deploying it.