Val Town: write and deploy code in seconds
A hands-on tutorial for Val Town, the platform where you write JavaScript and deploy it to a live URL instantly. APIs, cron jobs, email handlers, SQLite, blob storage, and more.
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.
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 and 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 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, 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 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:
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 or Cloudflare Workers, this is the same Request and Response you already know.
Let’s return JSON instead:
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:
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, a tiny web framework. You don’t install it, you import it straight from npm:
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:
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:
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:
import { sqlite } from 'https://esm.town/v/std/sqlite/main.ts'
Let’s build a tiny guestbook. First, the table:
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:
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:
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:
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:
import { blob } from 'https://esm.town/v/std/blob/main.ts'
The simplest use is reading and writing JSON:
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:
const keys = await blob.list('app_')
And delete one:
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.
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, andbccfields.
Send HTML instead of text if you want:
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:
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:
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:
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 [email protected]. 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:
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:
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:
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:
deno install -grAf jsr:@valtown/vt
Run vt once to log in.
Now create a val as a local folder:
vt create my-api
cd my-api
Edit the files in your editor, then push to go live:
vt push
The best part is vt watch. It syncs your changes every time you save:
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:
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:
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, it’s free to try.
Related posts about services: