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

Jamiu OkanlawonJamiu Okanlawon
4 min read

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 ConceptDart Shelf EquivalentThe Short Version
express()Router() or Pipeline()The main entry point for your app.
req, resRequest requestShelf 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 installdart pub addHow 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.

0
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.