plainweb

The all-in-one web framework obsessing
about velocity ๐ŸŽ๏ธ
3344 sparks

npx create-plainweb
  • repo/
    • โ”œโ”€โ”€
    • โ””โ”€โ”€ app/
      • โ”œโ”€โ”€
      • โ”œโ”€โ”€ routes/
        • โ”‚ โ””โ”€โ”€
      • โ”œโ”€โ”€ components/
        • โ”‚ โ””โ”€โ”€
      • โ”œโ”€โ”€ services/
        • โ”‚ โ””โ”€โ”€
      • โ”œโ”€โ”€ config/
        • โ”‚ โ”œโ”€โ”€
        • โ”‚ โ””โ”€โ”€
      • โ””โ”€โ”€ cli/
        •   โ””โ”€โ”€

import { zfd } from "zod-form-data";
import { type Handler } from "plainweb";
import { database } from "app/config/database";
import { contacts } from "app/schema";
import { createContact } from "app/services/contacts";
import { Form } from "app/components/form";

export const POST: Handler = async ({ req }) => {
  const parsed = zfd 
    .formData({ email: zfd.text().refine((e) => e.includes("@")) })
    .safeParse(req.body);

  if (!parsed.success) { 
    return <Form email={parsed.data.email} error="Invalid email" />;
  }

  await createContact(database, parsed.data.email); 
  return <div>Thanks for subscribing!</div>;
}

export const GET: Handler = async () => {
  return <Form />;
}


export interface FormProps {
  email?: string;
  error?: string;
}

export function Form(props: FormProps) {
  return ( 
    <form hx-post="/signup">
      <input type="email" name="email" value={props.email} />
      {props.error && <span>{props.error}</span>}
      <button>Subscribe</button>
    </form>
  );
}


import { sendMail } from "plainweb";
import { type Database } from "app/config/database";
import { contacts } from "app/schema";

export async function createContact(database: Database, email: string) {
   await sendMail({
     from: "[email protected]",
     to: email,
     subject: "Hey there",
     text: "Thanks for signing up!",
   });
   await database.insert(contacts).values({ email });
}


import { env } from "app/config/env";
import middleware from "app/config/middleware";
import * as schema from "app/schema";
import { defineConfig } from "plainweb";

export default defineConfig({
  nodeEnv: env.NODE_ENV,
  http: {
    port: env.PORT ?? 3000,
    staticPath: "/public",
    middleware,
  },
  database: {
    dbUrl: env.DB_URL ?? "db.sqlite3",
    schema: schema,
    pragma: {
      journal_mode: "WAL",
    },
  },
  mail: {
    default: {
      host: env.SMTP_HOST,
      port: 587,
      secure: false,
      auth: {
        user: env.SMTP_USER,
        pass: env.SMTP_PASS,
      },
    },
  },
});


import { text, sqliteTable, int } from "drizzle-orm/sqlite-core";

export const contacts = sqliteTable("contacts", {
  email: text("email").primaryKey(),
  created: int("created").notNull(),
  doubleOpted: int("double_opted"),
});

export type Contact = typeof contacts.$inferSelect;


import { getDatabase } from "plainweb";
import config from "plainweb.config";

export const database = getDatabase(config);
export type Database = typeof database;


import dotenv from "dotenv";
import z from "zod";

dotenv.config();

export const envSchema = z.object({
  NODE_ENV: z.enum(["development", "production", "test"]),
  PORT: z.coerce.number().default(3000),
  DB_URL: z.string().default("db.sqlite3")
})

export type Env = z.infer<typeof envSchema>;

export const env: Env = envSchema.parse(process.env);


import { getApp, log } from "plainweb";
import config from "plainweb.config";

async function serve() {
  const app = await getApp(config);
  app.listen(config.http.port);
  log.info(`โšก๏ธ http://localhost:${config.http.port}`);
}

serve();

Key features

Velocity is everything ๐ŸŽ๏ธ

Every design decision prioritizes your ability to ship fast.

Server-side rendering ๐Ÿ–ฅ๏ธ

Compose and render JSX on the server. Fully type-safe.

No bundle.js ๐Ÿš€

Sprinkle HTMX and Alpine.js on top. No frontend build process.

Streaming ๐ŸŒŠ

Stream responses using <Suspense/> without client-side JavaScript.

File-based routing ๐Ÿ“

The file system determines the URL paths. No more naming routes twice.

Easy deployment ๐Ÿ”Œ

A single process to deploy and manage.

Type-safe SQL ๐Ÿ›ก๏ธ

Type-safe SQL query builder that gets out of your way.

Background Tasks โฑ๏ธ

Run persistent tasks in the background, concurrently or in-parallel.

Testable ๐Ÿงช

Test services, components, routes, emails and tasks with ease.

Docs โ†’

Stay up to date

Receive ~2 updates a month, no spam, unsubscribe anytime.