Skip to main content

ToDo App with a V backend

We can all (mostly) agree that the 'Hello World' example is quintessential when it comes to learning any programming language. It immediately (almost deceptively as well) gives you a quick glimpse into the language in which it is written. Another almost ubiquitous example is the 'ToDo' application, which is used (mostly) to showcase front-end frameworks. I will borrow a leaf from the latter, and walk through how to create a simple ToDo backend using V as the programing language.

Setting up the V application#

If you have not yet installed V, I will refer you to the V documentation for more complete details. Assuming that you have V installed and running, let initialize a new projects

mkdir v-todo-appcd v-todo-app
v initChange the description of your project in `v.mod`Complete!
#or
v new#follow the prompts to complete the setup

This creates and barebone project, containing a v.mod file for your project metadata and v-todo-app.v file which contains the main module. In V, the name of a module does not have to be the same as the file in which it is defined

Create the Repository Interface#

Define a Task struct in the module. The choice of using a struct data type here is purely for convenience and illustration, and you could model this in any way that fits your style. Also bear in mind that type and function definitions (fn, struct, interface etc) are done outside the main function

struct Task {    name string    done bool}

Let's define the interface into which we will add the repository functions. We'll look at one function at a time

interface Repo {    save(task &Task)}

This function will accept a task and persist it. Remember that variables in V are only localized within the scope they are defined, shadowing is not permitted and there are no global variables. That will influence how the application will look like when built

Implement the Repository interface (in-memory)#

Now let's handle an implementation of the Repo interface by the TaskRepo struct, which will handle the actual persistence A type implements an interface by implementing its methods and fields. There is no explicit declaration of intent, no "implements" keyword.

struct TaskRepo {    name stringmut:    store []Task}

The choice is defining a name property in TaskRepo is very deliberate, even though it's completely unnecessary. I wanted to contrast how you define a mutable vs immutable property in a struct. The store property is a collection of tasks, so it needs to grow depending on the storage requirements.

save()#

Now let's implement the save function for TaskRepo.

fn (mut repo TaskRepo) save(task &Task) {    repo.store << task}

In the implementation, the argument repo has to be defined using mut keyword, so that it may be usable inside the function. In the function, the implmentation simply appends or pushes a task into the existing mutable array

In the main() function, you can quickly test this out to make sure you are doing good.

fn main() {    mut repo := TaskRepo{}    read := Task{'read', false}    println(read)    repo.save(read)    println(repo)}

find()#

With that out of the way, let/s add a second method to the TaskRepo interface

find(name string) Task

The function will take a name argument and return the first task whose name matches the provided value. Let's implement this function

fn (mut repo TaskRepo) find(name string) Task {    return repo.store.filter(it.name == name).first()}

The Array type in V has a wide variety of helper functions which are worth spending some time looking at. Now update the main function to test out this new function

repo.save(read)    by_name := repo.find('read')println(by_name)

find_all()#

Now let's add another interface method to TaskRepo which will delegate the filtering responsibility to the user.

find_all(fn (&Task) bool) []Task

The function will accept a reference (hence to weird & sign in the argument definition), and will return a true/false response which will be use to include or exclude an item in the final result.

Implementing this function is straight-forward as well.

fn (mut repo TaskRepo) find_all(apply fn (&Task) bool) []Task {    return repo.store.filter(apply(it))}

The apply argument in the find_all function a weird looking type, but it really is just the function type used in the interface method definition. It's probably also worth mentioning that the it variable is implicitly available in Array functions and represents the current array item in the iteration.

To test this new addition, add a filter function outside the main body

fn by_filter (task &Task) bool {    return task.name == 'laugh'}

Adjust the main method to look like this

fn main() {    mut repo := TaskRepo{}    eat := Task{'eat', false}    live := Task{'live', false}    laugh := Task{'laugh', false}    repo.save(eat)    repo.save(live)    repo.save(laugh)        by_name := repo.find('eat')    println(by_name)
    println(repo.find_all(by_filter))}

toggle()#

Now let's add another function to the TaskRepo interface

toggle(name string) []Task

This function take a name argument and toggles the done status of all the tasks whose name matches the provided value.

The implementation is also relatively straight-forward

fn (mut repo TaskRepo) toggle(name string) []Task {    repo.store = repo.store.map(toggle_task(name, it))    return repo.store.filter(it.name == name)}

The toggle operation will mutate to array in the TaskRepo implementation so that the change lives on beyong the lifetime of the toggle function. The map function is best handled using a helper function toggle_task. Outside the main function, as the helper function

fn toggle_task(name string, task Task) Task {    return match task.name {        name { Task{task.name, !(task.done)} }        else { task }    }}

The toggle_task function deserves some more commentary. First, the function uses the match construct. There's nothing special about it and there's definately no requirement to use it. You can equally use the if construct to achieve the same result. The big advantage of using the match expression is that you must be exhautive in your possible options, which forces you to pause and contemplate about all the possible scenarios.

The toggle_task takes a non-mutable Task. This is a constraint imposed by the map function, and it's a good one for preserving immutability. So the response is either a new Task created from the matched one, or the existing task if no match is found.

Now let's update the main function to test out these latest changes

finish_eating := repo.toggle('eat')println(finish_eating)

delete#

Now let's add the final method of the TaskRepo interface for this excercise - delete

delete(name string)

This method accepts a name argument and removes the matching tasks from the repository. The returned value is the number of matched (and therefore deleted) tasks

The implementation of this method is equally straight-forward

fn (mut repo TaskRepo) delete(name string) int {    size := repo.store.len    repo.store = repo.store.filter(it.name != name)    return size - repo.store.len}

Now add one task twek to the main method to test the delete function

drop_live := repo.delete('live')println('dropped $drop_live')

Refactor to use 'repository' module#

The repository is pretty much done, and a little refactoring at this point would help organize the code better, and clean up the main module.

Every file in the root of a folder is part of the same module. Simple programs don't need to specify module name, in which case it defaults to 'main'.

To create a new module, create a directory with your module's name containing .v files with code:

mkdir repotouch repo/repository.v

in the repository.v file, move the functionality of the repository. You will need to define some functions and members as public so that they are visible from the main module.

repo/repository.v
module repo
pub struct Task {pub:    name stringmut:    done bool}
interface Repo {    save(task &Task)    find(name string) Task    find_all(fn (&Task) bool) []Task    toggle(name string) []Task    delete(name string) int}
pub struct TaskRepo {    name stringmut:    store []Task}
pub fn (mut repo TaskRepo) save(task &Task) {    repo.store << task}
pub fn (mut repo TaskRepo) find(name string) Task {    return repo.store.filter(it.name == name).first()}
pub fn (mut repo TaskRepo) find_all(apply fn (&Task) bool) []Task {    return repo.store.filter(apply(it))}
pub fn (mut repo TaskRepo) toggle(name string) []Task {    repo.store = repo.store.map(toggle_task(name, it))    return repo.store.filter(it.name == name)}
pub fn (mut repo TaskRepo) delete(name string) int {    size := repo.store.len    repo.store = repo.store.filter(it.name != name)    return size - repo.store.len}
fn toggle_task(name string, task Task) Task {    return match task.name {        name { Task{task.name, !(task.done)} }        else { task }    }}

The main module now simply imports the repo module and makes use of its public interface. Everything thing shoud continue working as it did before

v-todo-app.v
module mainimport repo {Task, TaskRepo}
fn by_filter (task &Task) bool {    return task.name == 'laugh'}
fn main() {    mut repo := TaskRepo{}    eat := Task{'eat', false}    live := Task{'live', false}    laugh := Task{'laugh', false}    repo.save(eat)    repo.save(live)    repo.save(laugh)        by_name := repo.find('eat')    println(by_name)
    println(repo.find_all(by_filter))        finish_eating := repo.toggle('eat')    println(finish_eating)
    drop_live := repo.delete('live')    println('dropped $drop_live')}

Implement the Repository interface (sqlite)#

We previously implemented the Repository interface which kept the data in memory. From a pure functional programming perspective (and with V specifically), you will quickly learn that this model is unusable for practical purposes. Given that there is no global variables to hold application state, that there is no closures where you can tuck away a value to live beyond the lifetime of the scope it was defined, and that the memory allocated to create objects get freed up when the objects gets out of scope, then you will need to store the application state outside the lifetime of the process.

With that backdrop, let's handle a second implementation of the Repo interface by the TaskRepo struct, which will handle the actual persistence to a sqlite database

import sqlite module#

Create a new dbrepo folder and in it add a new file dbrepository.v. In this file, add a dbrepo module definition and import the sqlite module. This is how you can install the sqlite module is you haven't done so already. The Task struct and TaskRepo struct will remain the same as they are in the repo module. The Repo interface will have a few tweaks to accomodate the use of an actual database better

module dbrepo
import sqlite
pub struct Task {pub:    name stringmut:    done bool}
interface Repo {    save(task &Task) ?int    find(name string) ?Task    find_all() ?[]Task    find_all_maches(fn (&Task) bool) ?[]Task    toggle(name string) ?int    delete(name string) ?int}
pub struct TaskRepo {    name string}

initialize the database#

V has one awesome feature which is an initialization step for any module loaded into the application. This allows you to do some prep-work if need be, and this is a perfect place to add the code to initialize our database. This function is also conveniently named init. I have also added a connect method, which will be used by other functions that implement the Repo interface.

pub fn(mut repo TaskRepo) connect() ?sqlite.DB {    db := sqlite.connect('./todos.db') or {         return error('could not connect to repository')    }    return db}
pub fn (mut repo TaskRepo) init() {    db := repo.connect() or {        panic(err)    }    db.exec('drop table if exists tasks;')    db.exec('create table if not exists tasks (name name not null, done bool default 0);')    println('database initialized')}
fn row_to_task (row sqlite.Row) Task {    return Task{row.vals[0], row.vals[1] == '1'}}

For convenience, I have attached a connect() function to the TaskRepo struct that returns an Optional result. This should be handled using either an or or a ?. In either case, the application will simply return a generic error response to the web client. The other function, row_to_task(), is not visible outside the repo module and is used to map a database row to a Task struct

save()#

Now let's implement the save function for TaskRepo to persist to sqlite

pub fn (mut repo TaskRepo) save(task &Task) ?int {    db := repo.connect()?    _, b := db.exec("insert into tasks (name) values ('$task.name');")     println('saved result $b')    return b}

Once a connection is established, an insert query is executed. The db.exec function returns two values, the first being an array of db.Row objects and the second being a status code from the sqlite. Since the first result item is unused, it's ignored using an underscore and the function simply returns the status code

In the main() function, you can quickly test this out to make sure you are doing good.

fn main() {    mut repo := TaskRepo{}    read := Task{'read', false}    println(read)    repo.save(read)    println(repo)}

find()#

Moving on to the second method in the TaskRepo interface. The function will take a name argument and return the first task whose name matches the provided value. Let's implement this function

pub fn (mut repo TaskRepo) find(name string) ?Task {    db := repo.connect()?    row := db.exec_one("select * from tasks where name = '$name' limit 1;")?    println('find result $row')    return Task{row.vals[0], row.vals[1] == '1'}}

Once a connection is established, an insert query is executed. The db.exec_one function returns an optional value. I am using ? here to propagate the error handling to the caller of the find() function. Once a database record has been found, the row columns are mapped to a new Task struct instance

In the main() function, you can quickly test to check that all is good. The call to repo.find(...) must now handle the returned optional value. In this case, the program exits. If no record is found, then program exits as well

repo.save(read)    by_name := repo.find('read') or {    return}println(by_name)

find_all()#

Moving to the next method in the TaskRepo interface. The fucntion will simply return all the items in the database, even though it's not a wise function to put of there in practise.

Implementing this function is very straight-forward.

pub fn (mut repo TaskRepo) find_all() ?[]Task {    db := repo.connect()?    rows, _ := db.exec('select * from tasks;')    return rows.map(row_to_task)}

find_all_matches()#

Moving to the next method in the TaskRepo interface. The function will accept an anonymous function which will return a boolean response that response will be used to include or exclude an item in the final result.

Implementing this function is straight-forward as well.

pub fn (mut repo TaskRepo) find_all_maches(apply fn (&Task) bool) ?[]Task {    db := repo.connect()?    rows, _ := db.exec('select * from tasks;')    return rows.map(row_to_task).filter(apply(it))}

The apply argument in find_all function has a weird looking type, but it really is just the function type used in the interface method definition. It's probably also worth mentioning that the it variable is implicitly available in Array functions and represents the current array item in the iteration.

To test this new addition, adjust the main method to look like this

fn main() {    mut repo := TaskRepo{}    eat := Task{'eat', false}    live := Task{'live', false}    laugh := Task{'laugh', false}
    r1 := repo.save(eat)    println('r is $r1')
    r2 := repo.save(live)    println('r is $r2')
    r3 := repo.save(laugh)    println('r is $r3')
    println('find task')
    by_name := repo.find('eat') or {        return    }    println(by_name)
    all := repo.find_all_matches(fn (task &Task) bool {        return task.name == 'laugh'    }) or {        return    }    println(all)}

toggle()#

Moving to the next method in the TaskRepo interface. The function take a name argument and toggles the done status of all the tasks whose name matches the provided value.

The implementation is a little bit more convoluted but still clear enough to follow

pub fn (mut repo TaskRepo) toggle(name string, done bool) ?int {    db := repo.connect()?    _, status := db.exec("update tasks set done = ${done} where name = '${name}';")    return status}

Upon connecting to the database successfully, the toggle operation retrieve all records matching the given name. The for each record, the database record will be updated with to new done status. This is of course practically very inefficient, but it's basicaly good for illustration.

Now let's update the main function to test out these latest changes

finish_eating := repo.toggle('eat') or {    return}println(finish_eating)

delete#

Moving to the last method in the TaskRepo interface. This method accepts a name argument and removes the matching tasks from the repository. The returned value is the number of matched (and therefore deleted) tasks

The implementation of this method is equally straight-forward

pub fn (mut repo TaskRepo) delete(name string) ?int {    db := repo.connect()?    println('delete task with name $name')    _, status := db.exec("delete from tasks where name = '$name';")    return status}

Now add one task twek to the main method to test the delete function

drop_live := repo.delete('live') or {    return}println('dropped $drop_live')println('exiting')

The web server application#

Having played with the repository module, it's time to create a web application which will handle http requests and interract with a repository. V ships with an inbuilt web framework vweb which I evaluated alongside others I found in awesome v projects like valval and vex. Vex was clearly ahead of the curve in terms of completeness and features, so I will be using it here. Checkout this video for a quick intro.

To get started, in the main module, simply import the code modules from Vex and also the dbrepo module.

import nedpals.vex.serverimport nedpals.vex.routerimport nedpals.vex.ctximport dbrepo {Task, TaskRepo}

Set up application#

In the main function, create the entry point to the web application

//initialize the appmut app := router.new()
//[add app handlers here]
//start the appserver.serve(app, 8081)}

The app will accept route definitions which will handle request based on both the http method and request path of an incoming request. In a lot of ways, this framework closely mirrors express js, which is very refreshing.

Default handler#

app.route(.get, '/', fn (req &ctx.Req, mut res &ctx.Resp) {    res.send('You have reached Vex!', 200)})

This handler simply response with a message if you curl into the root path

Save handler#

The next logcal handler is one to accept a create new task request. In order to do this, we will need to make use of the repository, and therefore we'll need to make some small tweeks to the existing application.

First will be to introduce a new variable before the handlers to keep a reference to the TaskRepo. And as a workaround, since V does not yet handle closures (a anonymous function is unable to use variables defined in the same scope which it is defined), Vex gives us an escape-hatch where you can inject a value and then be able to access it from inside the anonymous function. This is the purpose of inject. You can get a reference to the repo later on inside the handlers using mut ctx_repo := req.ctx

mut repo = TaskRepo{}app.inject(repo)

For the handler that will create a new task, it should handle post requests to the /task path. Add this into the main function, and the create_new_task function outside of the main function

#outside main methodfn create_new_task(mut repo TaskRepo, req &ctx.Req, mut resp ctx.Resp) {    form_data := req.parse_form() or {        map[string]string{}    }    name := form_data['name']    println('create with name $name')    new_task := Task{name, false}    repo.save(new_task) or {        resp.send("problem creating new task", 400)        return    }    resp.send('Created', 201)}
#inside main methodapp.route(.post, '/task', fn (req &ctx.Req, mut resp ctx.Resp) {    mut ctx_repo := req.ctx    create_new_task(mut ctx_repo, req, mut resp)})

In the create_new_task function, we are able to retrieve the name attribute from the form data in the request. This assumes that the request is of application/x-www-form-urlencoded type. The response is simply a 201 status with 'Created' as the payload. You can inspect the other functions available in the vweb.Result object more closely in the source code.

curl -X POST \  http://localhost:8081/task \  -H 'cache-control: no-cache' \  -H 'content-type: application/x-www-form-urlencoded' \  -d name=Bake cake

Find task by name handler#

Now that we can create a new task, it's time to find a specific task by name. For this, let's create a find_task_by_name handler

#outside main methodfn find_task_by_name(mut repo TaskRepo, req &ctx.Req, mut resp ctx.Resp) {    name := req.params['name']    println('find by name $name')    task := repo.find(name) or {        resp.send("problem fetching task", 400)        return    }    resp.send_json(task, 200)}
#inside main methodapp.route(.get, '/task/name/:name', fn (req &ctx.Req, mut resp ctx.Resp) {    mut ctx_repo := req.ctx    find_task_by_name(mut ctx_repo, req, mut resp)})

Find all tasks handler#

Next, it's time to fetch all the tasks in the repo. For this, let's create a find_all handler

#outside main methodpub fn (mut repo TaskRepo) find_all() ?[]Task {    db := repo.connect()?    rows, _ := db.exec('select * from tasks;')    return rows.map(row_to_task)}
#inside main methodapp.route(.get, '/task/all', fn (req &ctx.Req, mut resp ctx.Resp) {    mut ctx_repo := req.ctx    find_all_tasks(mut ctx_repo, req, mut resp)})

Find matching tasks handler#

Next, it's time to fetch all the tasks in the repo that match the specified criteria. For this, let's create a find_all_maches handler

#outside main methodpub fn (mut repo TaskRepo) find_all_maches(apply fn (&Task) bool) ?[]Task {    db := repo.connect()?    rows, _ := db.exec('select * from tasks;')    return rows.map(row_to_task).filter(apply(it))}
#inside main methodapp.route(.get, '/task', fn (req &ctx.Req, mut resp ctx.Resp) {    mut ctx_repo := req.ctx    find_all_matches(mut ctx_repo, req, mut resp)})

Toggle task handler#

Next, it's time to toggle tha completion status of a task in the repo. For this, let's create a toggle handler

#outside main methodpub fn (mut repo TaskRepo) toggle(name string, done bool) ?int {    db := repo.connect()?    _, status := db.exec("update tasks set done = ${done} where name = '${name}';")    return status}
#inside main methodapp.route(.put, '/task', fn (req &ctx.Req, mut resp ctx.Resp) {    mut ctx_repo := req.ctx    toggle_task_status(mut ctx_repo, req, mut resp)})

Delete task handler#

Last but not least, it's time to delete a task from the repo. For this, let's create a delete_task handler

#outside main methodfn delete_task(mut repo TaskRepo, req &ctx.Req, mut resp ctx.Resp) {    println('delete tasks with matching name')    name := req.params['name']    tasks := repo.delete(name) or {        resp.send("problem deleting tasks", 400)        return    }    resp.send_json(tasks, 200)}
#inside main methodapp.route(.delete, '/task/:name', fn (req &ctx.Req, mut resp ctx.Resp) {    mut ctx_repo := req.ctx    delete_task(mut ctx_repo, req, mut resp)})

I hope this was helpful in some way. I will be churning out more articles on V-Lang, so keep an eye out for more content. To reach me directly, you can easily do so through Twitter, Facebook or Discord.