npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

eslint-plugin-solodev

v1.0.0

Published

10 ESLint rules that replace code review for solo developers. Typed errors, safe Zod parsing, server action contracts, and more.

Readme

eslint-plugin-solodev

10 ESLint rules that replace code review for solo developers.

When there's no one to catch your throw new Error() or your empty catch {}, these rules do it for you. Built from real patterns that caused production bugs in a one-person SaaS.

Install

npm install eslint-plugin-solodev --save-dev

Setup (flat config)

// eslint.config.mjs
import solodev from 'eslint-plugin-solodev'

export default [
  // All 7 framework-agnostic rules
  solodev.configs.recommended,

  // OR: all 10 rules including Next.js server action rules
  solodev.configs.nextjs,
]

Or pick individual rules:

import solodev from 'eslint-plugin-solodev'

export default [
  {
    plugins: { solodev },
    rules: {
      'solodev/no-plain-error-throw': 'error',
      'solodev/no-schema-parse': 'error',
    }
  }
]

Rules

| Rule | Category | Description | |---|---|---| | no-plain-error-throw | Error handling | Ban throw new Error() — force typed subclasses | | no-silent-catch | Error handling | Ban empty catch + log-only catch blocks | | no-schema-parse | Zod safety | Ban schema.parse() — force safeParse() | | no-unsafe-type-assertions | Type safety | Ban as unknown as Type double-casts | | no-unvalidated-formdata | Type safety | Ban formData.get('x') as string | | no-loose-status-type | Type safety | Ban status: string — force union types | | one-export-per-file | Code organization | One exported function per file | | action-must-return | Server actions | Actions must return via response helpers | | require-use-server | Server actions | .action.ts files must start with "use server" | | prefer-server-actions | Server actions | Flag fetch('/api/...') — use server actions |


no-plain-error-throw

A plain Error tells you nothing at 3am. Was it a network timeout you should retry? A validation failure you should surface? A bug you should page yourself for? Typed error subclasses encode this directly.

// Bad
throw new Error('Payment failed')
reject(new Error('Timed out'))

// Good
throw new TransientError('Payment gateway timeout', { cause: error })
throw new FatalError('Invalid card number')

no-silent-catch

Most linters catch empty catch {}. This rule also catches the more insidious pattern: logging the error and continuing as if nothing happened.

// Bad
catch (e) {}
catch (e) { console.error(e) }
promise.catch(() => {})
promise.catch(e => console.log(e))

// Good
catch (e) { logger.error(e); throw e }
catch (e) { return fallbackValue }

no-schema-parse

Zod's .parse() throws a ZodError on failure. In a server action, that's an uncontrolled exception with a useless stack trace. safeParse() returns a discriminated union you can handle gracefully.

// Bad
const data = userSchema.parse(input)
z.array(schema).parse(items)

// Good
const result = userSchema.safeParse(input)
if (!result.success) return failed(result.error)
const data = result.data

Skips test files automatically.

no-unsafe-type-assertions

as unknown as Type is TypeScript's escape hatch. It silently breaks every type guarantee. If you need to convert between types, validate with a schema or write a type guard.

// Bad
const user = data as unknown as User

// Good
const result = userSchema.safeParse(data)

no-unvalidated-formdata

FormData.get() returns FormDataEntryValue | null. Casting to string hides a null that will blow up at runtime.

// Bad
const email = formData.get('email') as string

// Good
const email = formData.get('email')
if (!email || typeof email !== 'string') throw new Error('Missing email')

no-loose-status-type

status: string accepts any string. A typo like "actve" compiles fine and silently corrupts your data.

// Bad
type Order = { status: string }
type Order = { status: string | null }

// Good
type Order = { status: 'pending' | 'paid' | 'shipped' }

one-export-per-file

When you're solo, discoverability IS your architecture. One function per file means the filename tells you everything.

// Bad — two-exports.ts
export function createUser() { ... }
export function deleteUser() { ... }

// Good — create-user.ts
export function createUser() { ... }

Options:

'solodev/one-export-per-file': ['error', { max: 2 }]

action-must-return

A server action that doesn't return a typed response is invisible to the client. Did it succeed? Fail? The caller will never know.

// Bad — silent success
export async function updateProfile(data: FormData) {
  await db.update(data)
}

// Good
export async function updateProfile(data: FormData) {
  await db.update(data)
  return actionSuccessResponse('Profile updated')
}

Options:

'solodev/action-must-return': ['error', {
  returnFunctions: ['actionSuccessResponse', 'actionFailureResponse'],
  filePattern: '.action.ts'
}]

require-use-server

Without the "use server" directive, Next.js silently treats your action file as a regular module. It works in dev, breaks in prod.

// Bad — missing directive
export async function createUser() { ... }

// Good
"use server"
export async function createUser() { ... }

Options:

'solodev/require-use-server': ['error', { filePattern: '.server.ts' }]

prefer-server-actions

fetch('/api/users') gives you untyped JSON and string URL typos. Server actions give you end-to-end TypeScript safety.

// Bad
const res = await fetch('/api/users')
const data = await res.json() // untyped

// Good
const result = await getUsers() // fully typed input + output

Options:

'solodev/prefer-server-actions': ['error', {
  internalPatterns: ['/api/', '/trpc/']
}]

Skips external URLs and test files automatically.


Companion rules

These patterns are useful but don't need a custom plugin — use built-in ESLint config:

Prefer safe JSON parse

// eslint.config.mjs
{
  rules: {
    'no-restricted-syntax': ['error', {
      selector: "CallExpression[callee.object.name='JSON'][callee.property.name='parse']:not([arguments.1])",
      message: 'Use safeJsonParse() or pass a reviver to JSON.parse().'
    }]
  }
}

Prefer Promise.allSettled

{
  rules: {
    'no-restricted-syntax': ['error', {
      selector: "CallExpression[callee.object.name='Promise'][callee.property.name='all']",
      message: 'Prefer Promise.allSettled() — Promise.all() rejects on first failure and loses other results.'
    }]
  }
}

No direct process.env

Use the n/no-process-env rule from eslint-plugin-n.

Consistent logging (no console)

{
  rules: {
    'no-console': ['error', { allow: ['warn'] }]
  }
}

Typographic quotes

Use eslint-plugin-human — auto-fixes straight quotes to curly in JSX.


Philosophy

These rules exist because solo developers don't get code review. Every pattern here caused a real production bug that would have been caught by a second pair of eyes:

  • A throw new Error() that should have been retried
  • A catch (e) { console.log(e) } that silently broke a payment flow
  • A .parse() that crashed a server action with an unreadable ZodError
  • An as unknown as that passed TypeScript but failed at runtime
  • A status: string that accepted a typo and corrupted 200 records

If you work with a team, you probably don't need all of these. If you work alone, turn them all on and never look back.

License

MIT