Lucifer is a fast, light-weight web framework in dart.

Related tags

Templates lucifer
Overview

Lucifer Lightbringer

Lucifer is a fast, light-weight web framework in dart.

It's built on top of native HttpServer to provide a simple way to fulfill the needs of many modern web server this day.

Lucifer is open, efficient, and has lots of built in features to perform dozen kinds of things.

Installation

Install Dart SDK

You can start create a new project with lucy command.

$ dart pub global activate lucy

$ lucy create desire

The first command will activate lucifer command-line interface (CLI), named lucy, to be accessible in your terminal. And then lucy create desire creates a new project named desire in the desire directory.

Feel free to use any project name you want.

Starting

Now we are ready to build our web server.

Open file main.dart in your project bin directory and see the structure of a simple lucifer application.

import 'package:lucifer/lucifer.dart';

void main() {
  final app = App();
  final port = env('PORT') ?? 3000;

  app.use(logger());

  app.get('/', (Req req, Res res) async {
    await res.send('Hello Detective');
  });

  await app.listen(port);
  print('Server running at http://${app.host}:${app.port}');
  app.checkRoutes();
}

Test running it with command.

$ cd desire

$ lucy run

And open URL http://localhost:3000 in your browser.

If all went well, it will display Hello Detective and print this message in your terminal.

Server running at http://localhost:3000

Fundamentals

We can learn the basic of Lucifer by understanding the main.dart code that's generated by lucy create command.

import 'package:lucifer/lucifer.dart';

void main() async {
  final app = App();
  final port = env('PORT') ?? 3000;

  app.use(logger());

  app.get('/', (Req req, Res res) async {
    await res.send('Hello Detective');
  });

  await app.listen(port);
  
  print('Server running at http://${app.host}:${app.port}');
  app.checkRoutes();
}

Those short lines of code do several things behind the scene.

First, we import lucifer and create a web application by assigning a new App object to app

import 'package:lucifer/lucifer.dart';
final app = App();

Then we set the server port to 3000 from .env file that's located in your root project directory.

Feel free to change with any port number you want.

final port = env('PORT') ?? 3000;

Once we have the app object, we tell it to listen to a GET request on path / by using app.get()

app.get('/', (Req req, Res res) async {
  
});

Every HTTP verbs has its own methods in Lucifer: get(), post(), put(), delete(), patch() with the first argument corresponds to the route path.

app.get('/', (Req req, Res res) {

});

app.post('/', (Req req, Res res) {

});

app.put('/', (Req req, Res res) {

});

app.delete('/', (Req req, Res res) {

});

app.patch('/', (Req req, Res res) {

});

Next to it, we can see a callback function that will be called when an incoming request is processed, and send a response with it.

To handle the incoming request and send a response, we can write our code inside.

app.get('/', (Req req, Res res) async {
  await res.send('Hello Detective');
});

Lucifer provides two objects, req and res, that represents Req and Res instance.

Req is an HTTP request built on top of native HttpRequest. It holds all information about the incoming request, such as request parameters, query string, headers, body, and much more.

Res is an HTTP response built on top of native HttpResponse. It's mostly used for manipulating response to be sent to the client.

What we did before is sending a message string Hello Detective to the client using res.send(). This method sets the string in the response body, and then close the connection.

The last line of our code starts the server and listen to the incoming requests on the specified port

await app.listen(port);
print('Server running at http://${app.host}:${app.port}');

Alternatively, we can use app.listen() as follows.

await app.listen(port, '127.0.0.1');

await app.listen(port, () {
  print('Server running at http://${app.host}:${app.port}');
});

await app.listen(port, 'localhost', () {
  print('Server running at http://${app.host}:${app.port}');
});

Environment Variables

Environment is some set of variables known to a process (such as, ENV, PORT, etc). It's recommended to mimic production environment during development by reading it from .env file.

When we use lucy create command, a .env file is created in the root project directory that contains these default values.

ENV = development
PORT = 3000

Then it's called from the dart code using env() method.

void main() {
  final app = App();
  final port = env('PORT') ?? 3000; // get port from env

  // get ENV to check if it's in a development or production stage
  final environment = env('ENV'); 

  ...
}

For our own security, we should use environment variables for important things, such as database configurations and JSON Web Token (JWT) secret.

And one more thing, if you open .gitignore file in the root directory, you can see .env is included there. It means our .env file will not be uploaded to remote repository like github, and the values inside will never be exposed to the pubic.

Request Parameters

A simple reference to all the request object properties and how to use them.

We've learned before that the req object holds the HTTP request informations. There are some properties of req that you will likely access in your application.

Property Description
app holds reference to the Lucifer app object
uriString URI string of the request
path the URL path
method the HTTP method being used
params the route named parameters
query a map object containing all the query string used in the request
body contains the data submitted in the request body (must be parsed before you can access it)
cookies contains the cookies sent by the request (needs the `cookieParser` middleware)
protocol The request protocol (http or https)
secure true if request is secure (using HTTPS)

GET Query String

Now we'll see how to retrieve the GET query parameters.

Query string is the part that comes after URL path, and starts with a question mark ? like ?username=lucifer.

Multiple query parameters can be added with character &.

?username=lucifer&age=10000

How can we get those values?

Lucifer provides a req.query object to make it easy to get those query values.

app.get('/', (Req req, Res res) {
  print(req.query);
});

This object contains map of each query parameter.

If there are no query, it will be an empty map or {}.

We can easily iterate on it with for loop. This will print each query key and its value.

for (var key in req.query.keys) {
  var value = req.query[key];
  print('Query $key: $value');
}

Or you can access the value directly with res.q()

req.q('username'); // same as req.query['username']

req.q('age'); // same as req.query['age']

POST Request Data

POST request data are sent by HTTP clients, such as from HTML form, or from a POST request using Postman or from JavaScript code.

How can we access these data?

If it's sent as json with Content-Type: application/json, we need to use json() middleware.

final app = App();

// use json middleware to parse json request body
// usually sent from REST API
app.use(json());
// use xssClean to clean the inputs
app.use(xssClean());

If it's sent as urlencoded Content-Type: application/x-www-form-urlencoded, use urlencoded() middleware.

final app = App();

// use urlencoded middleware to parse urlencoded request body
// usually sent from HTML form
app.use(urlencoded());
// use xssClean to clean the inputs
app.use(xssClean());

Now we can access the data from req.body

app.post('/login', (Req req, Res res) {
  final username = req.body['username'];
  final password = req.body['password'];
});

or simply use req.data()

app.post('/login', (Req req, Res res) {
  final username = req.data('username');
  final password = req.data('password');
});

Besides json() and urlencoded(), there are other available built in body parsers we can use.

  • raw() : to get request body as raw bytes
  • text() : to get request body as a plain string
  • json() : to parse json request body
  • urlencoded() : to parse urlencoded request body
  • multipart() : to parse multipart request body

To ensure the core framework stays lightweight, Lucifer will not assume anything about your request body. So you can choose and apply the appropriate parser as needed in your application.

However, if you want to be safe and need to be able to handle all forms of request body, simply use the bodyParser() middleware.

final app = App();

app.use(bodyParser());

It will automatically detect the type of request body, and use the appropriate parser accordingly for each incoming request.

Send Response

In the example above, we've used res.send() to send a simple response to the client.

app.get('/', (Req req, Res res) async {
  await res.send('Hello Detective');
});

If you pass a string, lucifer will set Content-Type header to text/html.

If you pass a map or list object, it will set as application/json, and encode the data into JSON.

res.send() sets the correct Content-Length response header automatically.

res.send() also close the connection when it's all done.

You can use res.end() method to send an empty response without any content in the response body.

app.get('/', (Req req, Res res) async {
  await res.end();
});

Another thing is you can send the data directly without res.send()

app.get('/string', (req, res) => 'string');

app.get('/int', (req, res) => 25);

app.get('/double', (req, res) => 3.14);

app.get('/json', (req, res) => { 'name': 'lucifer' });

app.get('/list', (req, res) => ['Lucifer',  'Detective']);

HTTP Status Response

You can set HTTP status response using res.status() method.

res.status(404).end();

or

res.status(404).send('Not Found');

Or simply use res.sendStatus()

// shortcut for res.status(200).send('OK');
res.sendStatus(200); 

// shortcut for res.status(403).send('Forbidden');
res.sendStatus(403);

// shortcut for res.status(404).send('Not Found');
res.sendStatus(404);

// shortcut for res.status(500).send('Internal Server Error');
res.sendStatus(500);

JSON Response

Besides res.send() method we've used before, we can use res.json() to send json data to the client.

It accepts a map or list object, and automatically encode it into json string with jsonEncode()

res.json({ 'name': 'Lucifer', 'age': 10000 });
res.json(['Lucifer', 'Detective', 'Amenadiel']);

Cookies

Use res.cookie() to manage cookies in your application.

res.cookie('username', 'Lucifer');

This method accepts additional parameters with various options.

res.cookie(
  'username', 
  'Lucifer', 
  domain: '.luciferinheaven.com',
  path: '/admin',
  secure: true,
);
res.cookie(
  'username',
  'Lucifer',
  expires: Duration(milliseconds: DateTime.now().millisecondsSinceEpoch + 900000),
  httpOnly: true,
);

Here is some cookie parameters you can eat.

Value Type Description
domain String Domain name for the cookie. Defaults to the domain name of the app
expires Date Expiry date of the cookie in GMT. If not specified or set to 0, creates a session cookie that will be deleted when client close the browser.
httpOnly bool Flags the cookie to be accessible only by the web server
maxAge int Convenient option for setting the expiry time relative to the current time in milliseconds
path String Path for the cookie. Defaults to /
secure bool Marks the cookie to be used with HTTPS only
signed bool Indicates if the cookie should be signed
sameSite bool or String Set the value of SameSite cookie

A cookie can be deleted with

res.clearCookie('username');

Or to clear all cookies.

res.clearCookies();

Secure Cookies

You can secure cookies in your application using secureCookie() middleware.

String cookieSecret = env('COOKIE_SECRET_KEY');

app.use(secureCookie(cookieSecret));

COOKIE_SECRET_KEY needs to be set in the .env file and should be a random string unique to your application.

HTTP Headers

We can get HTTP header of a request from req.headers

app.get('/', (req, res) {
  print(req.headers);
});

Or we use req.get() to get an individual header value.

app.get('/', (req, res) {
  final userAgent = req.get('User-Agent');

  // same as 

  req.header('User-Agent');
});

To change HTTP header of a response to the client, we can use res.set() and res.header()

res.set('Content-Type', 'text/html');

// same as 

res.header('Content-Type', 'text/html');

There are other ways to handle the Content-Type header.

res.type('.html'); // res.set('Content-Type', 'text/html');

res.type('html'); // res.set('Content-Type', 'text/html');

res.type('json'); // res.set('Content-Type', 'application/json');

res.type('application/json'); // res.set('Content-Type', 'application/json');

res.type('png'); // res.set('Content-Type', 'image/png');

Redirects

Using redirects are common thing to do in a web application. You can redirect a response in your application with res.redirect() or res.to()

res.redirect('/get-over-here');

// same as 

res.to('/get-over-here');

This will create redirect with a default 302 status code.

We can also use it this way.

res.redirect(301, '/get-over-here');

// same as 

res.to(301, '/get-over-here');

You can pass the path with an absolute path (/get-over-here), an absolute URL (https://scorpio.com/get-over-here), a relative path (get-over-here), or .. to go back one level.

res.redirect('../get-over-here');

res.redirect('..');

Or simply use res.back() to redirect back to the previous url based on the HTTP Referer value sent by client in the request header (defaults to / if not set).

res.back();

Routing

Routing is the process of determining what should happen when a URL is called, and which parts of the application needs to handle the request.

In the example before we've used.

app.get('/', (req, res) async {

});

This creates a route that maps root path / with HTTP GET method to the response we provide inside the callback function.

We can use named parameters to listen for custom request.

Say we want to provide a profile API that accepts a string as username, and return the user details. We want the string parameter to be part of the URL (not as query string).

So we use named parameters like this.

app.get('/profile/:username', (Req req, Res res) {
  // get username from URL parameter
  final username = req.params['username'];

  print(username);
});

You can use multiple parameters in the same URL, and it will automatically added to req.params values.

As an alternative, you can use req.param() to access an individual value of req.params

app.get('/profile/:username', (Req req, Res res) {
  // get username from URL parameter
  final username = req.param('username');

  print(username);
});

Advanced Routing

We can use Router object from app.router() to build an organized routing.

final app = App();
final router = app.router();

router.get('/login', (req, res) async {
  await res.send('Login Page');
});

app.use('/auth', router);

Now the login page will be available at http://localhost:3000/auth/login.

You can register more than one routers in your app.

final app = App();

final auth = app.router();
final user = app.router();

// register routes for auth

auth.get('/login', (Req req, Res res) async {
  await res.send('Login Page');
});

auth.post('/login', (Req req, Res res) async {
  // process POST login
});

auth.get('/logout', (Req req, Res res) async {
  // process logout
});

// register routes for user

user.get('/', (Req req, Res res) async {
  await res.send('List User');
});

user.get('/:id', (Req req, Res res) async {
  final id = req.param('id');
  await res.send('Profile $id');
});

user.post('/', (Req req, Res res) async {  
  // create user
});

user.put('/:id', (Req req, Res res) async {
  // edit user by id
});

user.delete('/', (Req req, Res res) async {
  // delete all users
});

user.delete(':id', (Req req, Res res) async {
  // delete user
});

// apply the router
app.use('/auth', auth);
app.use('/user', user);

Using app.router() is a good practice to organize your endpoints. You can split them into independent files to maintain a clean, structured and easy-to-read code.

Another way to organize your app is using app.route()

final app = App();

app.route('/user')
  .get('/', (Req req, Res res) async {
    await res.send('List User');
  })
  .get('/:id', (Req req, Res res) async {
    final id = req.param('id');
    await res.send('Profile $id');
  })
  .post('/', (Req req, Res res) async {
    // create user
  })
  .put('/:id', (Req req, Res res) async {
    // edit user by id
  })
  .delete('/', (Req req, Res res) async {
    // delete all users
  })
  .delete('/:id', (Req req, Res res) async {
    // delete user
  });

With this app.route() you can also use Controller. This is especially useful when you're building a REST API.

Lets create a new controller in the controller directory.

class UserController extends Controller {
  UserController(App app) : super(app);

  @override
  FutureOr index(Req req, Res res) async {
    await res.send('User List');
  }

  @override
  FutureOr view(Req req, Res res) async {
    await res.send('User Detail');
  }

  @override
  FutureOr create(Req req, Res res) async {
    await res.send('Create User');
  }

  @override
  FutureOr edit(Req req, Res res) async {
    await res.send('Edit User');
  }

  @override
  FutureOr delete(Req req, Res res) async {
    await res.send('Delete User');
  }

  @override
  FutureOr deleteAll(Req req, Res res) async {
    await res.send('Delete All Users');
  }
}

Then use it in your main app like this.

final app = App();
final user = UserController(app);

// This will add all associated routes for all methods
app.route('/user', user);

// The 1-line code above is the same as 
// manually adding these yourself
app.route('/user')
  .get('/', user.index)
  .post('/', user.create)
  .delete('/', user.deleteAll)
  .get('/:id', user.view)
  .put('/:id', user.edit)
  .delete('/:id', user.delete);

It's good practice to split your routes into its own independent controllers.

Also, feel free to add more methods to your Controller

class UserController extends Controller {

  ...

  FutureOr vip(Req req, Res res) async {
    await res.send('List of VIP Users');
  }
}

Then apply the method by chaining app.route()

final app = App();
final user = UserController(app);

// this will add route GET /user/vip into your app
// along with all the standard routes above
app.route('/user', user).get('/vip', user.vip);

To help you with adding Controller to your project, Lucifer provides another command like this.

$ lucy c post

These command will create a post_controller.dart file in the /bin/controller directory, and automatically fill it with a boilerplate PostController class.

You can use it to create more than one Controller like this.

$ lucy c post news user customer

Static Files

It's common to have images, css, and javascripts in a public folder, and expose them.

You can do it by using static() middleware.

final app = App();

app.use(static('public'));

Now if you have index.html file in the public directory, it will be served automatically when you hit http://localhost:3000.

Sending Files

Lucifer provides a simple way to send file as an attachment to the client with res.download()

When user hit a route that sends file with this method, browsers will prompt the user for download. Instead of showing it in a browser, it will be saved into local disk.

app.get('/downloadfile', (Req req, Res res) async {
  await res.download('thefile.pdf');

  // same as

  await res.sendFile('thefile.pdf');
});

You can send a file with a custom filename.

app.get('/downloadfile', (Req req, Res res) async {
  await res.download('thefile.pdf', 'File.pdf');
});

And to handle the error when sending file, use this.

app.get('/downloadfile', (Req req, Res res) async {
  final err = await res.download('./thefile.pdf', 'File.pdf');

  if (err != null) {
    // handle error
  }
});

CORS

A client app running in the browser usually can access only the resources from the same domain (origin) as the server.

Loading images or scripts/styles usually works, but XHR and Fetch calls to another server will fail, unless the server implements a way to allow that connection.

That way is CORS (Cross-Origin Resource Sharing).

Loading web fonts using @font-face also has same-origin-policy by default, and also other less popular things (like WebGL textures).

If you don't set up a CORS policy that allows 3rd party origins, their requests will fail.

A cross origin request fail if it's sent

  • to a different domain
  • to a different subdomain
  • to a different port
  • to a different protocol

and it's there for your own security, to prevent any malicious users from exploiting your resources.

But if you control both the server and the client, you have good reasons to allow them to talk with each other.

Use cors middleware to set up the CORS policy.

As example, lets say you have a simple route without cors.

final app = App();

app.get('/no-cors', (Req req, Res res) async {
  await res.send('Risky without CORS');
});

If you hit /no-cors using fetch request from a different origin, it will raise a CORS issue.

All you need to make it work is using the built in cors middleware and pass it to the request handler.

final app = App();

app.get('/yes-cors', cors(), (Req req, Res res) async {
  await res.send('Now it works');
});

You can apply cors for all incoming requests by using app.use()

final app = App();

app.use(cors());

app.get('/', (Req req, Res res) async {
  await res.send('Now all routes will use cors');
});

By default, cors will set cross-origin header to accept any incoming requests. You can change it to only allow one origin and block all the others.

final app = App();

app.use(cors(
  origin: 'https://luciferinheaven.com'
));

app.get('/', (Req req, Res res) async {
  await res.send('Now all routes can only accept request from https://luciferinheaven.com');
});

You can also set it up to allow multiple origins.

final app = App();

app.use(cors(
  origin: [
    'https://yourfirstdomain.com',
    'https://yourseconddomain.com',
  ],
));

app.get('/', (Req req, Res res) async {
  await res.send('Now all routes can accept request from both origins');
});

Session

We need to use sessions to identify client across many requests.

By default, web requests are stateless, sequential and two requests can't be linked to each other. There is no way to know if a request comes from a client that has already performed another request before.

Users can't be identified unless we use some kind of magic that makes it possible.

This is what sessions are (JSON Web Token is another).

When handled correctly, each user of your application or your API will be assigned to a unique session ID, and it allows you to store the user state.

We can use built in session middleware in lucifer.

final app = App();

app.use(session(secret: 'super-s3cr3t-key'));

And now all requests in your app will use session.

secret is the only required parameter, but there are many more you can use. secret should use a random string, unique to your application. Or use a generated string from randomkeygen.

This session is now active and attached to the request. And you can access it from req.session()

app.get('/', (Req req, Res res) {
  print(req.session()); // print all session values
});

To get a specific value from the session, you can use req.session(name)

final username = req.session('username');

Or use req.session(name, value) to add (or replace) value in the session.

final username = 'lucifer';

req.session('username', username);

Sessions can be used to to communicate data between middlewares, or to retrieve it later on the next request.

Where do we store this session?

Well, it depends on the set up that we use for our sessions.

It can be stored in

  • memory: this is the default, but don't use it in production
  • database: like Postgres, SQLite, MySQL or MongoDB
  • memory cache: like Redis or Memcached

All the session store above will only set session ID in a cookie, and keep the real data server-side.

Clients will receive this session id, and send it back with each of their next HTTP requests. Then the server can use it to get the store data associated with these session.

Memory is the default setting for session, it's pretty simple and requires zero setup on your part. However, it's not recommended for production.

The most efficient is using memory cache like Redis, but it needs some more efforts on your part to set up the infrastructure.

JSON Web Token

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.

This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

You can use JWT feature in Lucifer by using an instance of Jwt to sign and verify token. Remember to put the jwt secret in environment variables.

final app = App();
final port = env('PORT') ?? 3000;

final jwt = Jwt();
final secret = env('JWT_SECRET');

app.get('/login', (Req req, Res res) {

  ...

  final payload = <String, dynamic>{
    'username': 'lucifer',
    'age': 10000,
  };

  final token = jwt.sign(
    payload, 
    secret, 
    expiresIn: Duration(seconds: 86400),
  );

  // Send token to the client by putting it  
  // into 'x-access-token' header
  res.header('x-access-token', token);

  ...

});

Use jwt.verify() to verify the token.

final app = App();
final port = env('PORT') ?? 3000;

final jwt = Jwt();
final secret = env('JWT_SECRET');

app.get('/', (Req req, Res res) {
  // Get token from 'x-access-token' header
  final token = req.header('x-access-token');

  try {
    final data = jwt.verify(token, secret);

    if (data != null) {
      print(data['username']);
    }
  } on JWTExpiredError {
    // handle JWTExpiredError
  } on JWTError catch (e) {
    // handle JWTError
  } on Exception catch (e) {
    // handle Exception
  }

  ...

});

Another way to verify the token.

final app = App();
final port = env('PORT') ?? 3000;

final jwt = Jwt();
final secret = env('JWT_SECRET');

app.get('/', (Req req, Res res) {
  // Get token from client 'x-access-token' header
  final token = req.header('x-access-token');

  jwt.verify(token, secret, (error, data) {
    if (data != null) {
      print(data['username']);
    }

    if (error != null) {
      print(error);
    }
  });

  ...

});

Middleware

Middleware is function that hooks into the routing process. It performs some operations before executing the route callback handler.

Middleware is usually to edit the request or response object, or terminate the request before it reach the route callback.

You can add middleware like this.

app.use((Req req, Res res) async {
  // do something
});

This is a bit similar as defining the route callback.

Most of the time, you'll be enough by using built in middlewares provided by Lucifer, like the static, cors, session that we've used before.

But if you need it, you can easily create your own middleware, and use it for a specific route by putting it in the middle between route and callback handler.

final app = App();

// create custom middleware
final custom = (Req req, Res res) async {
  // do something here
};

// use the middleware for GET / request
app.get('/', custom, (Req req, Res res) async {
  await res.send('angels');
});

You can apply multiple middlewares to any route you want.

final app = App();

final verifyToken = (Req req, Res res) async {
  // do something here
};

final authorize = (Req req, Res res) async {
  // do something here
};

app.get('/user', [ verifyToken, authorize ], (req, res) async {
  await res.send('angels');
});

If you want to save data in a middleware and access it from the next middlewares or from the route callback, use res.local()

final app = App();

final verifyToken = (Req req, Res res) async {
  // saving token into the local data
  res.local('token', 'jwt-token');
};

final authorize = (Req req, Res res) async {
  // get token from local data
  var token = res.local('token');
};

app.get('/user', [ verifyToken, authorize ], (req, res) async {
  // get token from local data
  var token = res.local('token');
});

There is no next() to call in these middleware (unlike other framework like Express).

Processing next is handled automatically by lucifer.

A lucifer app will always run to the next middleware or callback in the current processing stack...

Unless, you send some response to the client in the middleware, which will close the connection and automatically stop all executions of the next middlewares/callback.

Since these call is automatic, you need to remember to use a proper async await when calling asynchronous functions.

As example, when using res.download() to send file to the client.

app.get('/download', (Req req, Res res) async {
  await res.download('somefile.pdf');
});

One simple rule to follow: if you call a function that returns Future or FutureOr, play safe and use async await.

If in the middle of testing your application, you see an error in the console with some message like HTTP headers not mutable or headers already sent, it's an indicator that some parts of your application need to use proper async await

Forms

Now lets learn to process forms with Lucifer.

Say we have an HTML form:

">
<form method="POST" action="/login">
  <input type="text" name="username" />
  <input type="password" name="password" />
  <input type="submit" value="Login" />
form>

When user press the submit button, browser will automatically make a POST request to /login in the same origin of the page, and with it sending some data to the server, encoded as application/x-www-form-urlencoded.

In this case, the data contains username and password

Form also can send data with GET, but mostly it will use the standard & safe POST method.

These data will be attached in the request body. To extract it, you can use the built in urlencoded middleware.

final app = App();

app.use(urlencoded());
// always use xssClean to clean the inputs
app.use(xssClean());

We can test create a POST endpoint for /login, and the submitted data will be available from req.body

app.post('/login', (Req req, Res res) async {
  final username = req.body['username']; // same as req.data('username');
  final password = req.body['password']; // same as req.data('password');

  ...

  await res.end();
});

File Uploads

Learn to handle uploading file(s) via forms.

Lets say you have an HTML form that allows user to upload file.

">
<form method="POST" action="/upload">
  <input type="file" name="document" />
  <input type="submit" value="Upload" />
form>

When user press the submit button, browser will automatically send a POST request to /upload in the same origin, and sending file from the input file.

It's not sent as application/x-www-form-urlencoded like the usual standard form, but multipart/form-data

Handling multipart data manually can be tricky and error prone, so we will use a built in FormParser utility that you can access with app.form()

final app = App();
final form = app.form();

app.post('/upload', (Req req, Res res) async {
  await form.parse(req, (error, fields, files) {
    if (error) {
      print('$error');
    }

    print(fields);
    print(files);
  });
});

You can use it per event that will be notified when each file is processed. This also notify other events, like on processing end, on receiving other non-file field, or when an error happened.

final app = App();
final form = app.form();

app.post('/upload', (Req req, Res res) async {
  await form
    .onField((name, field) {
      print('${name} ${field}');
    })
    .onFile((name, file) {
      print('${name} ${file.filename}');
    })
    .onError((error) {
      print('$error');
    })
    .onEnd(() {
      res.end();
    })
    .parse(req);
});

Or use it like this.

final app = App();
final form = app.form();

app.post('/upload', (Req req, Res res) async {
  await form
    .on('field', (name, field) {
      print('${name} ${field}');
    })
    .on('file', (name, file) {
      print('${name} ${file}');
    })
    .on('error', (error) {
      print('$error');
    })
    .on('end', () {
      res.end();
    })
    .parse(req);
});

Either way, you get one or more UploadedFile objects, which will give you information about the uploaded files. These are some value you can use.

  • file.name: to get the name from input file
  • file.filename: to get the filename
  • file.type: to get the MIME type of the file
  • file.data: to get raw byte data of the uploaded file

By default, FormParser only include the raw bytes data and not save it into any temporary folder. You can easily handle this yourself like this.

import 'package:path/path.dart' as path;

// file is an UploadedFile object you get before

// save to uploads directory
String uploads = path.absolute('uploads');

// use the same filename as sent by the client,
// but feel free to use other file naming strategy
File f = File('$uploads/${file.filename}');

// check if the file exists at uploads directory
bool exists = await f.exists();

// create file if not exists
if (!exists) {
  await f.create(recursive: true);
}

// write bytes data into the file
await f.writeAsBytes(file.data);

print('File is saved at ${f.path}');

Templating

Lucifer provides default templating with Mustache engine. It uses a package mustache_template that's implemented from the official mustache spec.

By default, to keep the core framework light-weight, lucifer doesn't attach any template engine to your app. To use the mustache middleware, you need to apply it first.

final app = App();

app.use(mustache());

Then these mustache can render any template you have in the project views directory.

Say you have this index.html in the views directory.

>
<html>
  <head>head>
  <body>
    <h2>{{ title }}h2>
  body>
html>

And render the template with res.render()

final app = App();

app.use(mustache());

app.get('/', (Req req, Res res) async {
  await res.render('index', { 'title': 'Hello Detective' });
});

If you run command lucy run and open http://localhost:3000 in the browser, it'll shows an html page displaying Hello Detective

You can change the default views with other directory you want.

final app = App();

// now use 'template' as the views directory
app.use(mustache('template'));

app.get('/', (Req req, Res res) async {
  // can also use res.view()
  await res.view('index', { 'title': 'Hello Detective' });
});

Now if you add this index.html file to the template directory.

>
<html>
  <head>head>
  <body>
    <h2>{{ title }} from templateh2>
  body>
html>

Then run the app and open it in the browser, it will shows another html page containing Hello Detective from template

For more complete details to use Mustache template engine, you can read the mustache manual.

To use other engines, such as jinja or jaded, you can manage the template rendering yourself, and then send the html by calling res.send()

app.get('/', (Req req, Res res) async {
  // render your jinja/jaded template into 'html' variable
  // then send it to the client
  await res.send(html);
});

Or you can create a custom middleware to handle templating with your chosen engine.

Here is example you can learn from the mustache middleware to create your own custom templating.

// 
// name it with anything you want
// 
Callback customTemplating([String? views]) {
  return (Req req, Res res) {
    // 
    // you need to overwrite res.renderer
    // using the chosen template engine
    //
    res.renderer = (String view, Map<String, dynamic> data) async {
      // 
      // most of the time, these 2 lines will stay
      // 
      String directory = views ?? 'views';
      File file = File('$directory/$view.html');

      // 
      // file checking also stay
      // 
      if (await file.exists()) {
        // 
        // mostly, all you need to do is edit these two lines 
        // 
        Template template = Template(await file.readAsString());
        String html = template.renderString(data);

        // 
        // in the end, always send the rendered html
        //
        await res.send(html);
      }
    };
  };
}

To apply the new templating middleware, use app.use() like before.

final app = App();

app.use(customTemplating());

Security

Lucifer has a built in security middleware that covers dozens of standard security protections to guard your application. To use them, simply add it to your app with app.use()

final app = App();

app.use(security());

Read here to learn more about web security.

Error Handling

Lucifer automatically handle the errors that occured in your application. However, you can set your own error handling with app.on()

final app = App();

app.on(404, (req, res) {
  // handle 404 Not Found Error in here
  // such as, showing a custom 404 page
});

// another way is using StatusCode
app.on(StatusCode.NOT_FOUND, (req, res) { });
app.on(StatusCode.INTERNAL_SERVER_ERROR, (req, res) { });
app.on(StatusCode.BAD_REQUEST, (req, res) { });
app.on(StatusCode.UNAUTHORIZED, (req, res) { });
app.on(StatusCode.PAYMENT_REQUIRED, (req, res) { });
app.on(StatusCode.FORBIDDEN, (req, res) { });
app.on(StatusCode.METHOD_NOT_ALLOWED, (req, res) { });
app.on(StatusCode.REQUEST_TIMEOUT, (req, res) { });
app.on(StatusCode.CONFLICT, (req, res) { });
app.on(StatusCode.UNPROCESSABLE_ENTITY, (req, res) { });
app.on(StatusCode.NOT_IMPLEMENTED, (req, res) { });
app.on(StatusCode.SERVICE_UNAVAILABLE, (req, res) { });

You can trigger HTTP exceptions from middleware or callback function.

app.get('/unauthorized', (Req req, Res res) async {
  throw UnauthorizedException();
});

Here is the list of all default exceptions we can use.

BadRequestException
UnauthorizedException
PaymentRequiredException
ForbiddenException
NotFoundException
MethodNotAllowedException
RequestTimeoutException
ConflictException
UnprocessableException
InternalErrorException
NotImplementedException
ServiceUnavailableException

Parallel Processing

Parallel and multithread-ing is supported by default with Dart/Lucifer. It can be done by distributing the processes evenly in various isolates.

Here is one way to do it.

import 'dart:async';
import 'dart:isolate';

import 'package:lucifer/lucifer.dart';

void main() async {
  // Start an app
  await startApp();

  // Spawn 10 new app with each own isolate
  for (int i = 0; i < 10; i++) {
    Isolate.spawn(spawnApp, null);
  }
}

void spawnApp(data) async {
  await startApp();
}

Future<App> startApp() async {
  final app = App();
  final port = env('PORT') ?? 3000;

  app.get('/', (Req req, Res res) async {
    await res.send('Hello Detective');
  });

  await app.listen(port);
  print('Server running at http://${app.host}:${app.port}');

  return app;
}

Web Socket

Web socket is a necessary part of web application if you need persistent communications between client and server. Here is an example to use web socket with Lucifer.

import 'dart:io';

import 'package:lucifer/lucifer.dart';

void main() async {
  final app = App();
  final port = env('PORT') ?? 3000;

  app.use(static('public'));

  app.get('/', (Req req, Res res) async {
    await res.sendFile('chat.html');
  });

  app.get('/ws', (Req req, Res res) async {
    List clients = [];

    final socket = app.socket(req, res);

    socket.on('open', (WebSocket client) {
      clients.add(client);
      for (var c in clients) {
        if (c != client) {
          c.send('New human has joined the chat');
        }
      }
    });
    socket.on('close', (WebSocket client) {
      clients.remove(client);
      for (var c in clients) {
        c.send('A human just left the chat');
      }
    });
    socket.on('message', (WebSocket client, message) {
      for (var c in clients) {
        if (c != client) {
          c.send(message);
        }
      }
    });
    socket.on('error', (WebSocket client, error) {
      res.log('$error');
    });
    
    await socket.listen();
  });

  await app.listen(port);
  print('Server running at http://${app.host}:${app.port}');
}

Contributions

Feel free to contribute to the project in any ways. This includes code reviews, pull requests, documentations, tutorials, or reporting bugs that you found in Lucifer.

License

MIT License

Copyright (c) 2021 Lucifer

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

You might also like...

Dart Web API Template Using Google Shelf Framework and Postgres Drivers, read readme to learn how to use

Shelf Web Framework Template by Alex Merced of AlexMercedCoder.com Cloning the Template git clone https://github.com/AlexMercedCoder/DartShelfPostgres

Dec 26, 2022

Simple and fast Entity-Component-System (ECS) library written in Dart.

Simple and fast Entity-Component-System (ECS) library written in Dart.

Fast ECS Simple and fast Entity-Component-System (ECS) library written in Dart. CPU Flame Chart Demonstration of performance for rotation of 1024 enti

Nov 16, 2022

Create dart data classes easily, fast and without writing boilerplate or running code generation.

Create dart data classes easily, fast and without writing boilerplate or running code generation.

Dart Data Class Generator Create dart data classes easily, fast and without writing boilerplate or running code generation. Features The generator can

Feb 28, 2022

Fast math TeX renderer for Flutter written in Dart.

Fast math TeX renderer for Flutter written in Dart.

CaTeX is a Flutter package that outputs TeX equations (like LaTeX, KaTeX, MathJax, etc.) inline using a widget and Flutter only - no plugins, no web v

Nov 14, 2022

Lightweight and blazing fast key-value database written in pure Dart.

Lightweight and blazing fast key-value database written in pure Dart.

Fast, Enjoyable & Secure NoSQL Database Hive is a lightweight and blazing fast key-value database written in pure Dart. Inspired by Bitcask. Documenta

Dec 30, 2022

Lightweight and blazing fast key-value database written in pure Dart.

Lightweight and blazing fast key-value database written in pure Dart.

Fast, Enjoyable & Secure NoSQL Database Hive is a lightweight and blazing fast key-value database written in pure Dart. Inspired by Bitcask. Documenta

Dec 30, 2022

Lite-graphql - A light way implementation of GraphQL client in dart language

lite GraphQL client A light way implementation of GraphQL client in dart languag

Mar 17, 2022

Fast and idiomatic UUIDs in Dart.

neouuid Fast and idiomatic UUIDs (Universally Unique Identifiers) in Dart. This library decodes and generates UUIDs, 128-bits represented as 32 hexade

Aug 23, 2022

A fast, flexible, IoC library for Dart and Flutter

Qinject A fast, flexible, IoC library for Dart and Flutter Qinject helps you easily develop applications using DI (Dependency Injection) and Service L

Sep 9, 2022
Comments
  • File uploads do not work as documentation examples shows

    File uploads do not work as documentation examples shows

    at https://pub.dev/packages/lucifer#file-uploads the documentation says second example:

    final app = App();
    final form = app.form();
    
    app.post('/upload', (Req req, Res res) async {
      await form
        .onField((name, field) {
          print('${name} ${field}');
        })
        .onFile((name, file) {
          print('${name} ${file.filename}');
        })
        .onError((error) {
          print('$error');
        })
        .onEnd(() {
          res.end();
        })
        .parse(req);
    });
    

    or third example:

    final app = App();
    final form = app.form();
    
    app.post('/upload', (Req req, Res res) async {
      await form
        .on('field', (name, field) {
          print('${name} ${field}');
        })
        .on('file', (name, file) {
          print('${name} ${file}');
        })
        .on('error', (error) {
          print('$error');
        })
        .on('end', () {
          res.end();
        })
        .parse(req);
    });
    
    

    in both cases onFile and on('file' ...) are not triggered.

    in .pub-cache/hosted/pub.dartlang.org/lucifer-1.0.7/lib/src/parsers/form_parser.dart line 76 / 78 listener is always null.

    The first example doesn't work either

    
    final app = App();
    final form = app.form();
    
    app.post('/upload', (Req req, Res res) async {
      await form.parse(req, (error, fields, files) {
        if (error) {
          print('$error');
        }
    
        print(fields);
        print(files);
      });
    });
    

    the line if (error) {. should be if (error != null) {

    opened by robert-boulanger 0
Owner
Salman S
Telle est la Voie
Salman S
A fast, extra light and synchronous key-value storage to Get framework

get_storage A fast, extra light and synchronous key-value in memory, which backs up data to disk at each operation. It is written entirely in Dart and

Jonny Borges 257 Dec 21, 2022
Command Line Interface (CLI) for Lucifer

Lucy Command Line Interface (CLI) for Lucifer. Installation Activate command line from your terminal with this command. pub global activate lucy Usage

Salman S 1 Dec 16, 2021
A composable, light-weight package that can be used as a placeholder whenever you need some fake data

API Placeholder A composable, light-weight package that can be used as a placeholder whenever you need some fake data. With this package, you can get

ASMIT VIMAL 2 Feb 27, 2022
Fast and productive web framework provided by Dart

See https://github.com/angulardart for current updates on this project. Packages Source code Published Version angular angular_forms angular_router an

Angular Dart Open Source Packages 1.9k Dec 15, 2022
A zero-dependency web framework for writing web apps in plain Dart.

Rad Rad is a frontend framework for creating fast and interactive web apps using Dart. It's inspired from Flutter and shares same programming paradigm

null 70 Dec 13, 2022
Flying Fish is full-stack Dart framework - a semi-opinionated framework for building applications exclusively using Dart and Flutter

Flying Fish is full-stack Dart framework - a semi-opinionated framework for building applications exclusively using Dart and Flutter.

Flutter Fish 3 Dec 27, 2022
App to control your health activities like calorie, water, medicine consumption, sleeping and weight control.

Handy Configuration for yourself This project contains google-services.json file of my own. You can connect your own firebase project using the follow

KanZa Studio 104 Jan 3, 2023
Weight tracker - OurPass interview challenge

Weight tracker - OurPass Screenshots Tools Used flutter: Google's UI toolkit for building cross platform apps firebase: Google's BAAS provider: State

Stanley Akpama 3 May 31, 2022
Intel Corporation 238 Dec 24, 2022
The ROHD Verification Framework is a hardware verification framework built upon ROHD for building testbenches.

ROHD Verification Framework The ROHD Verification Framework (ROHD-VF) is a verification framework built upon the Rapid Open Hardware Development (ROHD

Intel Corporation 18 Dec 20, 2022