# Val Town: write and deploy code in seconds

> A hands-on tutorial for Val Town, where you write JavaScript and deploy it to a live URL instantly, with HTTP, cron, and email triggers plus SQLite storage.

Author: Flavio Copes | Published: 2026-06-12 | Canonical: https://flaviocopes.com/val-town/

You write a bit of code, hit save, and a second later it's live on a URL. No server to set up, no deploy step, no config files.

That's [Val Town](https://www.val.town/).

I use it for small APIs, webhooks, and scheduled scripts. It's the fastest way I know to go from an idea to code running on the web.

In this post we'll build a few things with it, from your first endpoint to storing data and sending email.

## What is Val Town?

Val Town is a place to write [JavaScript](https://flaviocopes.com/javascript/) and [TypeScript](https://flaviocopes.com/typescript/) in the browser. When you save, your code is live on a URL.

The unit of code is called a **val**. Think of a val as a small folder of code that runs in the cloud. Like a [GitHub](https://flaviocopes.com/github/) repo, except it actually runs.

A file inside a val can have a **trigger**. The trigger is what makes your code run. There are three:

- **HTTP** — the code runs when someone visits a URL. This is how you build APIs and websites.
- **Cron** — the code runs on a schedule.
- **Email** — the code runs when an email arrives at a special address.

Val Town also gives you the things you usually need, ready to go: a database (SQLite), file storage, and a way to send email. Nothing to set up.

It runs on [Deno](https://flaviocopes.com/deno/), so you write modern JavaScript and TypeScript with web-standard APIs.

## Why I like it

It's fast. You save, it's live. The loop is seconds, not minutes.

Every save is a new version, so you can roll back the moment you break something.

There's no infrastructure to manage. The database, file storage, email, cron, HTTPS, the URL, it's all there already.

I reach for Val Town for APIs, webhooks, glue code between services, scheduled jobs, and quick demos. I wouldn't use it for heavy or long-running work. It's made for small, fast things.

## Getting started

Go to [val.town](https://www.val.town/) and sign up. It's free, no credit card.

You'll land in the editor. That's where you write and run vals, right in the browser.

## Your first val

Create a new val and give it an HTTP trigger, by clicking the `+` button at the top right of the editor and picking `HTTP`.

An HTTP val is a function. It gets a `Request` and returns a `Response`:

```ts
export default async function (req: Request): Promise<Response> {
  return new Response('Hello from Val Town!')
}
```

Save it. Val Town hands you a URL, and opening it shows your message. That's a live web server in about 100ms.

If you've used the [Fetch API](https://flaviocopes.com/fetch-api/) or [Cloudflare Workers](https://flaviocopes.com/cloudflare-workers/), this is the same `Request` and `Response` you already know.

Let's return JSON instead:

```ts
export default async function (req: Request): Promise<Response> {
  return Response.json({ ok: true })
}
```

`Response.json()` sets the content type and turns the object into JSON for you.

You can also read from the request. Here we grab a query parameter:

```ts
export default async function (req: Request): Promise<Response> {
  const url = new URL(req.url)
  const name = url.searchParams.get('name') || 'stranger'
  return new Response(`Hello, ${name}!`)
}
```

Add `?name=flavio` to your URL and it greets you by name. It's all standard web APIs, nothing Val Town specific.

## Building an API

One route in a plain function is fine. For multiple routes, a framework helps.

Val Town works well with [Hono](https://hono.dev/), a tiny web framework. You don't install it, you import it straight from npm:

```ts
import { Hono } from 'npm:hono'

const app = new Hono()

app.get('/', (c) => c.text('Home'))
app.get('/about', (c) => c.text('About'))

export default app.fetch
```

Look at the last line. `app.fetch` is a function that takes a `Request` and returns a `Response`, which is exactly what an HTTP val expects. So we hand it over as the default export.

You can read route parameters too:

```ts
app.get('/users/:name', (c) => {
  const name = c.req.param('name')
  return c.json({ user: name })
})
```

Visit `/users/flavio` and you get `{ "user": "flavio" }`.

You can return HTML by setting the content type and returning a string:

```ts
export default async function (): Promise<Response> {
  return new Response('<h1>My page</h1>', {
    headers: { 'Content-Type': 'text/html' },
  })
}
```

Val Town supports JSX too, if you want to build HTML with components. A plain string is enough to start.

## Storing data with SQLite

Most apps need to store something. Every val comes with its own private SQLite database. Each val gets its own, so two vals don't share data unless you want them to.

Import it from the standard library:

```ts
import { sqlite } from 'https://esm.town/v/std/sqlite/main.ts'
```

Let's build a tiny guestbook. First, the table:

```ts
await sqlite.execute(`create table if not exists messages (
  id integer primary key autoincrement,
  text text,
  created_at text default current_timestamp
)`)
```

`create table if not exists` means it's safe to run every time. It creates the table once and skips it after that.

To add a row, pass your SQL with arguments. The `?` is a placeholder for a value:

```ts
await sqlite.execute({
  sql: 'insert into messages (text) values (?)',
  args: ['Hello world!'],
})
```

Always use placeholders for values. They keep user input from messing with your query, which is how SQL injection happens.

To read the data back:

```ts
const result = await sqlite.execute('select text from messages order by id desc')
console.log(result.rows)
```

The rows are in `result.rows`. Each row is an object with your column names as keys.

Now let's put it in one HTTP val that lists messages and lets you add them:

```ts
import { sqlite } from 'https://esm.town/v/std/sqlite/main.ts'

await sqlite.execute(`create table if not exists messages (
  id integer primary key autoincrement,
  text text
)`)

export default async function (req: Request): Promise<Response> {
  if (req.method === 'POST') {
    const { text } = await req.json()
    await sqlite.execute({
      sql: 'insert into messages (text) values (?)',
      args: [text],
    })
  }

  const result = await sqlite.execute('select text from messages order by id desc')
  return Response.json(result.rows)
}
```

That's a full API with persistent storage in about 15 lines. POST a `text` field to add a message, GET to list them.

## Storing files with blob storage

SQLite is for structured data. When you want to stash a chunk of something, like JSON, an image, or a text file, use blob storage.

Import it the same way:

```ts
import { blob } from 'https://esm.town/v/std/blob/main.ts'
```

The simplest use is reading and writing JSON:

```ts
await blob.setJSON('settings', { theme: 'dark' })
const settings = await blob.getJSON('settings')
```

`getJSON` returns `undefined` if the key isn't there, so you can check for that.

You can list keys, optionally by prefix:

```ts
const keys = await blob.list('app_')
```

And delete one:

```ts
await blob.delete('settings')
```

I use blob storage a lot for small bits of state, like the last time a cron job ran.

## Sending email

Val Town can send email in one call. This is one of my favorite features.

```ts
import { email } from 'https://esm.town/v/std/email'

await email({
  subject: 'Hello from Val Town',
  text: 'This is a test email sent from a val.',
})
```

By default the email goes to you, the account owner. So sending yourself a notification is as easy as a `console.log`.

> Note: on the free plan you can only email yourself. On Pro you can email anyone, with the `to`, `cc`, and `bcc` fields.

Send HTML instead of text if you want:

```ts
await email({
  subject: 'Your daily report',
  html: '<h1>Today</h1><p>All systems normal.</p>',
})
```

## Running code on a schedule

A cron val runs on a timer. Good for reminders, polling a feed, or syncing data.

Give a file a `CRON` trigger, then write the function that runs:

```ts
import { email } from 'https://esm.town/v/std/email'

export default async function () {
  await email({
    subject: 'Good morning!',
    text: 'Time to start the day.',
  })
}
```

When you add the trigger, you pick the schedule. Either a simple interval like "once an hour", or a cron expression if you need more control.

This expression runs every two hours, on the first day of the month:

```
0 */2 1 * *
```

> Be careful with time zones. Cron in Val Town runs in UTC, so adjust if you mean a specific hour in your local time.

The function also gets an `interval` argument with the time it last ran. That's handy for "what's changed since last time" jobs:

```ts
export default async function (interval: Interval) {
  console.log('Last run was at', interval.lastRunAt)
}
```

## Receiving email

This is sending in reverse. An email val gets its own address, and your code runs whenever a message lands there.

Give a file an `EMAIL` trigger. The function gets the email as its argument:

```ts
export default async function (message: Email) {
  console.log('From:', message.from)
  console.log('Subject:', message.subject)
  console.log('Body:', message.text)
}
```

Val Town gives the val an address like `yourusername.valname@valtown.email`. Send a message there and the function runs.

The `message` object has what you'd expect: `from`, `to`, `subject`, `text`, `html`, and `attachments`.

Attachments are `File` objects, so you can read them:

```ts
export default async function (message: Email) {
  for (const file of message.attachments) {
    console.log(file.name, file.type)
    const content = await file.text()
    console.log(content)
  }
}
```

Put sending and receiving together and you can build email automations. Forward a message, post it to Slack, hand it to an AI. A few lines each.

## Importing npm packages

You saw this with Hono. Val Town supports thousands of npm packages, with no install step. You just import them:

```ts
import { Hono } from 'npm:hono'
import dayjs from 'npm:dayjs'
```

The `npm:` prefix tells Deno to fetch the package. No `package.json`, no `node_modules`.

You can also import from Deno's registry, and from other people's vals using `https://esm.town/` URLs, like we did for the standard library.

## Keeping secrets out of your code

You'll often need a secret, like an API key. Don't hardcode it in your val, because vals can be public.

Set it in your environment variables (there's a section for this in the Val Town interface), then read it in code:

```ts
const apiKey = Deno.env.get('OPENAI_API_KEY')
```

## Working from your own editor

The browser editor is great, but sometimes you want your own. Val Town has a CLI called `vt` for that.

It runs on Deno, so install Deno first, then the CLI:

```bash
deno install -grAf jsr:@valtown/vt
```

Run `vt` once to log in.

Now create a val as a local folder:

```bash
vt create my-api
cd my-api
```

Edit the files in your editor, then push to go live:

```bash
vt push
```

The best part is `vt watch`. It syncs your changes every time you save:

```bash
vt watch
```

So you keep the save-and-it's-live feeling, but in VS Code, Cursor, or wherever you work.

`vt` feels like `git`. You can clone a val, make branches, switch between them, and stream logs from your terminal:

```bash
vt clone username/valName
vt branch
vt tail
```

## Letting AI write your vals

Val Town has a built-in agent called **Townie** that writes and edits vals for you, in the browser.

If you'd rather use your own tools, there's a plugin that connects Val Town to Claude Code, Codex, and Cursor:

```bash
npx plugins add val-town/plugins
```

Now your AI assistant can create vals, edit them, read your SQLite data, and check logs, from your editor.

As always, read the code your AI writes, especially once it can touch your data.

## Pricing

The free plan is genuinely useful. You get HTTP, cron, email (to yourself), SQLite, and blob storage.

Pro adds what you need as you grow: emailing anyone, private vals, more storage, longer runs, and crons as often as every minute.

For learning and side projects, free is plenty.

## Try it

The thing that keeps me coming back is the speed. There's barely a gap between an idea and having it live.

So open the editor and build something small. A webhook, a reminder, a tiny API. You'll have it running before you finish your coffee.

Start at [val.town](https://www.val.town/), it's free to try.
