Dart Shelf for Express.js Developers: A Familiar Look at Backend Dart


If you're a Node.js developer, you probably live and breathe Express.js. Its simplicity, flexibility, and powerful middleware pattern have made it the undisputed king of Node backend frameworks. But what if you're curious about Dart, especially if you're using Flutter for your frontend? You might assume you have to learn a completely new set of backend concepts.
The good news is, you don't.
This guide is for you, the Express.js developer. We'll show you how Dart's Shelf framework uses concepts so similar to Express that you'll feel right at home. Let's see how your existing skills give you a massive head start.
The Core Idea: Mapping Concepts
The key to learning Shelf is realizing that the core concepts are the same, just with a different (and type-safe!) syntax.
Express.js Concept | Dart Shelf Equivalent | The Short Version |
express() | Router() or Pipeline() | The main entry point for your app. |
req , res | Request request | Shelf uses a single Request object and you return a Response . |
app.use(middleware) | Pipeline().addMiddleware() | The concept of using middleware is identical. |
req.params.id | (Request r, String id) | Route parameters are passed as function arguments. |
npm install | dart pub add | How you add your dependencies. |
1. Your First Server: From app.listen
to io.serve
In Express, you create an app instance and tell it to listen on a port.
app.js
(Express)
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello from Express!');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
Now, let's "translate" that to Dart and Shelf. Notice how the structure feels familiar. We create a handler for requests and serve it.
server.dart
(Shelf)
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
void main() async {
// This handler is like your (req, res) => { ... } callback
final handler = (Request request) {
return Response.ok('Hello from Shelf!');
};
// This is the equivalent of app.listen()
final server = await io.serve(handler, 'localhost', 3000);
print('Server running on port ${server.port}');
}
The core logic is the same: define what to do when a request comes in, and then start the server.
2. Routing: app.get
is Still app.get
Defining routes in Express is second nature. You use methods like app.get()
and app.post
()
. For routing in Shelf, we use the shelf_router
package, and you'll be shocked at how similar it is.
app.js
(Express)
// GET request
app.get('/users', (req, res) => {
res.send('Fetching all users');
});
// Route with a parameter
app.get('/users/:id', (req, res) => {
const { id } = req.params;
res.send(`Fetching user with ID: ${id}`);
});
Now, the Shelf equivalent. We just create a Router
instance, which feels a lot like the Express app
object.
server.dart
(Shelf with shelf_router
)
import 'package:shelf_router/shelf_router.dart';
// ... (imports and main function setup)
void main() async {
final app = Router();
// GET request
app.get('/users', (Request request) {
return Response.ok('Fetching all users');
});
// Route with a parameter - notice the <id> syntax
app.get('/users/<id>', (Request request, String id) {
return Response.ok('Fetching user with ID: $id');
});
await io.serve(app, 'localhost', 3000); // Serve the router
}
The method is still .get()
. The only real difference is the parameter syntax (:id
vs. <id>
) and how you access it (as a function argument instead of on a params
object).
3. Middleware: The Concept is Identical
Middleware is the heart of Express, and that concept translates perfectly to Shelf. In Express, you use app.use()
to apply middleware to all incoming requests.
app.js
(Express)
// A simple logger middleware
const logger = (req, res, next) => {
console.log(`${req.method} ${req.originalUrl}`);
next(); // Don't forget to call next()!
};
app.use(logger);
In Shelf, you use a Pipeline
to achieve the same thing. A pipeline lets you "pipe" a request through a series of middleware before it hits your router.
server.dart
(Shelf)
// A simple logger middleware
Middleware logger = (Handler innerHandler) {
return (Request request) {
print('${request.method} ${request.url}');
// The request is automatically passed to the next handler
return innerHandler(request);
};
};
// ... in main()
final myRouter = Router(); // Your router from the example above
// ...
final handler = const Pipeline()
.addMiddleware(logger) // This is the Shelf version of app.use()
.addHandler(myRouter); // The final destination for the request
await io.serve(handler, 'localhost', 3000);
Conclusion: You Already Know Backend Dart
While the syntax has its own Dart flavor, the fundamental patterns are the same. If you understand how to handle routes and link middleware in Express, you already understand how to do it in Shelf.
By leveraging your existing Express knowledge, the barrier to entry for backend Dart is incredibly low. You get to keep your expert understanding of web concepts while gaining the powerful benefits of Dart, like its excellent type safety and a unified language for both your frontend (Flutter) and backend.
Your Express skills haven't just prepared you for Node.js development; they've given you a head start in the Dart ecosystem, too.
Subscribe to my newsletter
Read articles from Jamiu Okanlawon directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Jamiu Okanlawon
Jamiu Okanlawon
Jamiu is a Flutter Google Developer Expert (GDE) and Mobile Engineer with over 6 years of experience building high-quality mobile apps. He founded the FlutterBytes Community to empower developers and actively contribute to the Flutter ecosystem worldwide.