Quick Start

Quick Start

Let's build a simple Express Todos app with TypeScript and Butterfly, using SQLite for storage! The finished code can be found in the examples/simple-express directory on GitHub.

For this project, we'll be using Pug for templating, because it's widely used and easy to read and write. Let's start by installing all of the dependencies we'll need:

Shell
npm i -D typescript @types/express
npm i express pug @aleksrutins/butterfly

Let's make a couple of scripts to make our life easier, and set "type": "module" to add ES module support:

package.json
{
    // ...
    "type": "module",
    // ...
    "scripts": {
        "build": "tsc",
        "start": "node dist/index.js",
        "db": "DATABASE_URL=sqlite3:data.db butterfly"
    }
    // ...
}

Now, let's initialize our database. Create a migrations directory with a 0-create-todos.up.sql file and a 0-create-todos.down.sql file. A todo has an integer ID and some text for the content, so put the following in the .up.sql file:

migrations/0-create-todos.up.sql
create table todos (
    id integer primary key autoincrement,
    content text
)

All the .down.sql file needs to do is drop the table:

migrations/0-create-todos.down.sql
drop table todos

Now, run the migration:

Shell
npm run db up

Now, let's start actually writing some code! Open a new file, and save it as src/index.ts. Let's start by importing our packages and creating our Express server:

src/index.ts
import { connect } from '@aleksrutins/butterfly';
import express from 'express';
 
const app = express();

We'll talk more about connect in a bit.

Now, let's tell TypeScript about the format of a todo:

src/index.ts
type Todo = {
    id: number,
    content: string
}

If you're using JavaScript, you don't need to declare your table types again outside of your migrations, but it's useful to make TypeScript happy.

Now, connect to the database and make sure it's connected:

src/index.ts
const db = await connect('sqlite3:data.db');
if(!db) throw "Failed to connect to database";

connect will take a URI, detect which driver you need (see the list of supported databases to see which protocols map to which drivers), and connect to the database, returning the driver instance. In this case, we're using SQLite3 with a file in the current directory called data.db, so we just pass sqlite3:data.db.

Hook up Pug and a static file server:

src/index.ts
app.set('view engine', 'pug');
 
app.use(express.static('static'));

Now, we reach the interesting part: let's make the homepage. Since this is a quick start for Butterfly, not for Pug or CSS, you can copy-paste the template and styles:

views/index.pug
doctype html
html(lang='en')
    head
        meta(name='viewport', content='width=device-width, initial-scale=1.0')
        title Todos 
        link(rel='stylesheet', href='/app.css')
    body 
        main 
            h1 Todos
            ul
                if todos.length == 0
                    li.no-todos You're all caught up!
                else 
                    each todo in todos 
                        li 
                            span= todo.content 
                            form(action='/delete', method='post')
                                input(type="hidden", name="id", value=todo.id)
                                button(type="submit").delete-btn Delete
            form(action="/create", method="post")#create-form
                input(type="text", name="content", placeholder="What do you need to do?", autofocus)
                button(type="submit")#add-btn Add
static/app.css
html, body {
    font-family: -apple-system, Cantarell, sans-serif;
    margin: 0;
    padding: 0;
    background-color: rgb(209, 209, 209);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    height: 100vh;
}
 
main {
    display: block;
    background-color: white;
    border-radius: 20px;
    padding: 25px;
}
 
h1 {
    text-align: center;
}
 
ul {
    border-radius: 20px;
    background-color: rgb(241, 241, 241);
    list-style-type: none;
    padding: 10px;
}
 
button {
    border-radius: 5px;
    padding: 10px 15px;
    border: none;
    cursor: pointer;
}
 
li:not(.no-todos) {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    margin: 2px;
}
 
li.no-todos {
    text-align: center;
    font-style: italic;
}
 
li span {
    margin-right: 5px;
}
 
li span, li form {
    display: block;
}
 
li button.delete-btn {
    background-color: red;
    color: white;
}
 
form#create-form {
    display: flex;
    flex-direction: row;
    justify-content: stretch;
}
 
form#create-form > * {
    display: block;
}
 
form#create-form > input {
    flex-grow: 1;
}
 
button#add-btn {
    background-color: green;
    color: white;
    margin-left: 3px;
}
 
input[type=text] {
    background-color: white;
    border: 1px solid rgb(201, 201, 201);
    border-radius: 5px;
    padding: 10px;
}

Now, if you're still here after all that CSS, let's get on to the logic! First, though, read this blurb:

There are two ways to query your database with Butterfly: you can either use the query<T> (returning a value) or exec (not returning a value) methods, or you can use the q<T> and e template tags. To explain the difference, I'll give an example; the two statements below do the exact same thing:

db.exec('INSERT INTO todos (content) VALUES (?)', [myContent]);
db.e`INSERT INTO todos (content) VALUES (${myContent})`;

There are three main advantages to using q and e over query and exec:

  1. They're shorter and easier to read.
  2. They protect from SQL injection without you, the programmer, having to worry about it.
  3. They're database-independent — for instance, although SQLite uses ? and PostgreSQL uses $<number> for parameters, you can use the same q or e call for either, while you would have to use two separate query or exec calls.

Because of those, his tutorial will use q and e, but you're free to use whichever fits your usecase best.

The / handler is relatively short:

src/index.ts
app.get('/', async (_req, res) => {
    const todos = await db.q<Todo>`SELECT * FROM todos`;
    res.render('index', { todos });
});

One important thing to note, however, is that the type parameter for q<T> is not an array! Both query and q will always return an array, so there's no need to specify it in the type parameter. Other than that, the handler should be relatively self-explanatory. (If it isn't, please make an issue on GitHub and tell me about it!)

The /create and /delete handlers are equally simple, although they use e instead of q:

src/index.ts
app.post('/create', express.urlencoded({ extended: false }), async (req, res) => {
    await db.e`INSERT INTO todos (content) VALUES (${req.body.content})`;
    res.redirect('/');
});
 
app.post('/delete', express.urlencoded({ extended: false }), async (req, res) => {
    await db.e`DELETE FROM todos WHERE id = ${req.body.id}`;
    res.redirect('/');
})

As explained above, q and e will automatically turn an interpolated string into safe query parameters, so there's no need to worry about SQL injection.

Now, all we have to is run the server and close the database when the program exits:

src/index.ts
const port = process.env['PORT'] ?? 8080;
app.listen(port, () => console.log(`Listening on ${port}`));
 
process.on('exit', db.destroy);

And we're done! Now, build and run:

Shell
npm run build
npm start

... and see your beautiful new todo app!

I'm not great at writing tutorials, so please submit issues if you encountered any difficulties!