How to Switch to uv: Replace pip, virtualenv, and Poetry in Your Python Project
uv replaces pip, virtualenv, and Poetry with a single fast binary. Step-by-step guide to migrating your existing Python project and setting up GitHub Actions CI.
On this page
Python package management has always been too complicated. To do it properly you needed pip to install packages, virtualenv or venv to isolate environments, pyenv to manage Python versions, and pip-tools to generate a proper lockfile. That's four separate tools, four different config formats, and four things that can break in CI.
uv fixes all of that. It's a single Rust binary from Astral (the same team that built ruff) that handles everything. Not "a little faster" fast: we're talking 10 to 100 times faster than pip on cold installs, and near-instant on warm ones.
This guide is specifically for migrating an existing project. If you're starting fresh, the workflow is even simpler, but most of us have a requirements.txt or pyproject.toml that needs to come along for the ride.
What uv Replaces
| Old tool | uv equivalent |
|---|---|
| `pip install requests` | `uv add requests` |
| `python -m venv .venv` | `uv venv` |
| `pyenv install 3.12` | `uv python install 3.12` |
| `pip-compile requirements.in` | `uv lock` |
| `pip install -r requirements.txt` | `uv sync` |
The Full Command Cheat Sheet
The table above covers the five commands you will type on day one. This one is the version worth keeping open in a browser tab for the first week, because it maps the whole surface area of pip, pip-tools, pyenv, pipx, and Poetry onto uv.
The pattern to internalise is that uv splits every operation into two halves: changing what your project declares (uv add, uv remove, uv lock) and making your environment match that declaration (uv sync, uv run). pip never had that split, which is exactly why pip install could quietly drift your environment away from your requirements file without anyone noticing until a deploy failed.
| Task | pip + venv | Poetry | uv |
|---|---|---|---|
| Create an environment | `python -m venv .venv` | `poetry install` | `uv venv` |
| Add a dependency | `pip install flask` | `poetry add flask` | `uv add flask` |
| Add a dev dependency | manual, second file | `poetry add --group dev pytest` | `uv add --dev pytest` |
| Remove a dependency | `pip uninstall flask` | `poetry remove flask` | `uv remove flask` |
| Install everything | `pip install -r requirements.txt` | `poetry install` | `uv sync` |
| Skip dev dependencies | second requirements file | `poetry install --without dev` | `uv sync --no-dev` |
| Regenerate the lockfile | `pip-compile` | `poetry lock` | `uv lock` |
| Upgrade one package | `pip-compile -P django` | `poetry update django` | `uv lock --upgrade-package django` |
| Upgrade everything | `pip-compile --upgrade` | `poetry update` | `uv lock --upgrade` |
| Run a command in the env | activate, then run | `poetry run pytest` | `uv run pytest` |
| Inspect the dependency tree | `pipdeptree` | `poetry show --tree` | `uv tree` |
| Install a Python version | `pyenv install 3.12` | not supported | `uv python install 3.12` |
| Pin the Python version | `pyenv local 3.12` | not supported | `uv python pin 3.12` |
| Install a global CLI tool | `pipx install ruff` | not supported | `uv tool install ruff` |
| Run a tool without installing | not supported | not supported | `uvx ruff check` |
| Build a distributable | `python -m build` | `poetry build` | `uv build` |
| Publish to PyPI | `twine upload dist/*` | `poetry publish` | `uv publish` |
Two rows deserve a footnote. uvx is not a separate program you have to install: it ships inside the uv binary and is shorthand for uv tool run. It creates a throwaway, cached environment, runs the tool, and leaves nothing behind in your project, which is why it is the right way to run one-off formatters, scaffolders, and migration scripts.
uv tree is the command most people discover last and then use constantly. It prints the resolved dependency graph for your project, and uv tree --invert flips it so you can ask the question you actually care about during an upgrade fight: which of my direct dependencies is dragging in this old version of urllib3?
Why uv Is This Much Faster
It is worth understanding where the speed comes from, because it changes how you use the tool. The gains are not one clever trick, they are four separate design decisions stacked on top of each other.
- It is a compiled binary, not a Python program. pip has to boot a Python interpreter and import its own dependency tree before it does any work. uv starts in single-digit milliseconds and has no import cost at all.
- It reads package metadata without downloading packages. To resolve a dependency graph you only need each wheel's metadata file, not the wheel. uv fetches just that slice over HTTP range requests, so resolution touches a fraction of the bytes pip would pull down.
- Downloads, extraction, and installation happen in parallel. pip processes packages largely one at a time. uv saturates your connection and your CPU cores, which is why the difference is most dramatic on large scientific stacks with dozens of transitive dependencies.
- There is one global cache, and environments link into it. uv keeps a content-addressed cache (usually
~/.cache/uv) and populates a virtual environment with hardlinks or copy-on-write clones instead of copying files.
That last point has a practical consequence worth changing your habits over. Because a .venv is mostly links into the shared cache, deleting and recreating it is close to free. If an environment ever looks wrong, do not debug it: run rm -rf .venv && uv sync and move on. With pip that was a two minute penalty, so people learned to poke at broken environments instead of rebuilding them.
It also means ten projects that all depend on the same version of numpy store that wheel once on disk, not ten times. The tradeoff is that hardlinks only work when the cache and the virtual environment live on the same filesystem, which is why Docker builds sometimes need UV_LINK_MODE=copy (covered in the troubleshooting section below).
- 1
Install uv
You don't need Python installed to install uv. It's a standalone binary.
bashcurl -LsSf https://astral.sh/uv/install.sh | shOn Windows (PowerShell):
powershellpowershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"Restart your terminal and run
uv --versionto confirm. - 2
Migrate from pip + requirements.txt
Navigate to your project root and initialize uv:
bashuv initThis creates a
pyproject.tomlif you don't already have one. Then import your existing requirements. If your requirements.txt is clean (no comments, no extras), you can do it in one line:bash — Quick importuv add $(cat requirements.txt | grep -v "^#" | xargs)Or use uv's pip-compatible interface for a file with pinned versions, comments, or extras:
bash — Safe alternativeuv pip install -r requirements.txtAfter that, run
uv lockto generate your lockfile. Commit bothpyproject.tomlanduv.lock. Going forward, useuv add package-nameinstead ofpip install, anduv run python script.pyinstead of activating the virtual environment manually. - 3
Migrate from Poetry
The easiest path is a tool called
migrate-to-uv, which runs viauvx(uv's tool runner, like npx for Python):bashuvx migrate-to-uvRun it in your project root. It reads your
pyproject.toml(Poetry format) and converts it to the uv format. Then rebuild:bashuv lock && uv syncA few things that don't auto-migrate cleanly:
- Optional dependency groups: Poetry uses
[tool.poetry.group.dev.dependencies]. uv uses[dependency-groups]with slightly different syntax. Check these manually after migration. - Private indexes: If your Poetry config references a private PyPI index, set it up in
[tool.uv.sources]in your newpyproject.toml. poetry runcalls: Replace allpoetry run Xwithuv run Xacross your scripts and documentation.
- Optional dependency groups: Poetry uses
- 4
Run Your Project
Once uv is managing your project, use
uv runfor everything. You don't need to activate the virtual environment manually:bash# Instead of: source .venv/bin/activate && python main.py uv run python main.py # Instead of: source .venv/bin/activate && pytest uv run pytest # Instead of: source .venv/bin/activate && flask run uv run flask run - 5
Pin Your Python Version
To pin a specific Python version for your project, create a
.python-versionfile in your project root. uv reads this file automatically:bashecho "3.12" > .python-versionYou can also install that Python version via uv if it's not already on your system:
bashuv python install 3.12 - 6
GitHub Actions CI
This is where uv's speed pays off most visibly. CI recreates your environment from scratch on every run: the difference between pip and uv is often 2 to 3 minutes vs 15 to 20 seconds.
yaml — .github/workflows/ci.ymlname: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v4 - name: Install Python run: uv python install - name: Install dependencies run: uv sync --frozen - name: Run tests run: uv run pytest tests/One addition worth making to that workflow:
astral-sh/setup-uvcan cache the uv download cache between runs. Turn it on withenable-cache: trueand CI installs drop from seconds to fractions of a second on cache hits.yaml — .github/workflows/ci.yml (cached)- uses: astral-sh/setup-uv@v4 with: enable-cache: true cache-dependency-glob: "uv.lock"The
cache-dependency-globtells the action to invalidate the cache only whenuv.lockchanges, which is exactly the right granularity. If you are building out a pipeline from scratch, the GitHub Actions tutorial covers the workflow syntax around this step in more detail. - 7
Build a Docker Image with uv
Docker is the other place uv pays for itself immediately, but only if you structure the Dockerfile so dependency installation is its own cacheable layer. The mistake almost everyone makes first is copying the whole project in before installing, which busts the dependency layer on every single source change.
The pattern below installs dependencies from
pyproject.tomlanduv.lockalone, then copies the application code afterwards. Edit a Python file and only the last two layers rebuild.dockerfile — DockerfileFROM python:3.12-slim-bookworm # Copy the uv binary straight out of the official image, pinned by tag COPY --from=ghcr.io/astral-sh/uv:0.9 /uv /uvx /bin/ WORKDIR /app ENV UV_COMPILE_BYTECODE=1 \ UV_LINK_MODE=copy # Layer 1: dependencies only. Cached until uv.lock or pyproject.toml changes. RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --locked --no-install-project --no-dev # Layer 2: your code, which changes on every commit COPY . /app RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-dev ENV PATH="/app/.venv/bin:$PATH" CMD ["uv", "run", "--no-sync", "gunicorn", "app.wsgi:application"]Four details in that file carry most of the benefit.
UV_COMPILE_BYTECODE=1precompiles.pycfiles during the build so your container does not pay that cost on first request, which matters for cold starts.UV_LINK_MODE=copydisables hardlinking, because the uv cache mount and the container filesystem are different mounts and hardlinks across them are impossible.--no-install-projectin the first layer installs your dependencies but not your own package, which is what keeps that layer stable across code changes. And the--mount=type=cacheline persists uv's download cache across builds without baking it into the image.For a smaller final image, use a multi-stage build: run the
uv syncsteps in a builder stage, then copy only/app/.venvinto a freshpython:3.12-slimstage. uv itself does not need to exist in the runtime image at all, as long as you put the virtual environment'sbindirectory onPATH. If you run these images under Podman rather than Docker, the Compose to Quadlet migration guide covers how the run-time side of that changes.
Migrating from Pipenv
Pipenv projects migrate more cleanly than most people expect, because Pipfile and Pipfile.lock carry the same information uv needs. The same migrate-to-uv tool used for Poetry detects a Pipfile automatically:
uvx migrate-to-uvIf you would rather do it by hand (worth it when your Pipfile has unusual markers or VCS dependencies), Pipenv can print your dependencies in requirements format and uv can read that file directly:
pipenv requirements > requirements.txt
pipenv requirements --dev-only > requirements-dev.txt
uv init
uv add -r requirements.txt
uv add --dev -r requirements-dev.txt
uv lock
uv syncOnce uv.lock is generated and your tests pass, delete Pipfile, Pipfile.lock, and the two temporary requirements files. Keeping them around invites someone to run pipenv install six months from now and quietly build a second environment.
The one feature with no direct replacement is Pipenv's [scripts] section. uv is a package manager, not a task runner, and it does not read a scripts table from pyproject.toml. You have three reasonable options.
- Declare real console entry points under
[project.scripts]inpyproject.toml. These become executables inside the environment and run withuv run my-command. This is the right choice for anything that is genuinely part of your package. - Keep a
Makefileor ajustfilewhere each target wraps auv run ...command. This is the most common pattern in uv codebases and it keeps task definitions out of your package metadata. - Use a dedicated task runner such as
poethepoet, installed as a dev dependency, if you want the tasks to live inpyproject.toml.
If you are coming from the JavaScript side and missing the ergonomics of a scripts block, the patterns in npm scripts you should know translate almost directly onto a Makefile wrapping uv run.
How the uv Lockfile Actually Works
uv.lock is not a pip freeze dump with a different name, and the difference matters. A frozen requirements file records what happened to be installed on one machine, running one Python version, on one operating system. Regenerate it on a colleague's laptop and you get a different file.
uv.lock is a universal lockfile. uv resolves your dependency graph for every platform and every Python version allowed by your requires-python range, then records all of those resolutions in one file with environment markers attached. A Linux CI runner and an Apple Silicon laptop read the same lockfile and each install the correct wheels from it.
version = 1
requires-python = ">=3.11"
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/.../requests-2.32.3.tar.gz", hash = "sha256:55365...", size = 131218 }
wheels = [
{ url = "https://files.pythonhosted.org/.../requests-2.32.3-py3-none-any.whl", hash = "sha256:70761...", size = 64928 },
]Every entry carries a hash, so an install is verified against the exact artifact that was resolved. If a package is ever yanked and republished with different contents, your install fails loudly instead of silently picking up different code.
The file is generated, so never hand-edit it. Change pyproject.toml, then re-lock. Note that uv add and uv remove update pyproject.toml, uv.lock, and your environment in a single command, so you rarely call uv lock directly outside of upgrades.
| Command | What it does | Use it when |
|---|---|---|
| `uv sync` | Re-locks if `pyproject.toml` changed, then installs | Everyday local development |
| `uv sync --locked` | Fails if `uv.lock` is out of date with `pyproject.toml` | CI, where a stale lockfile should break the build |
| `uv sync --frozen` | Installs `uv.lock` as-is, never re-resolves | Docker builds and deploys |
| `uv sync --no-dev` | Installs runtime dependencies only | Production images and release artifacts |
| `uv lock --check` | Verifies the lockfile is current without installing | A fast pre-commit or CI lint step |
| `uv lock --upgrade-package X` | Re-resolves one package, leaves the rest pinned | Targeted security patches |
The distinction between --locked and --frozen trips people up, so it is worth stating plainly. --locked verifies that the lockfile still matches pyproject.toml and errors if it does not. --frozen skips that check entirely and installs whatever the lockfile says. Use --locked in CI, where you want a forgotten uv lock to fail the pull request, and --frozen in a Docker build, where you may not even copy pyproject.toml into the layer.
If something outside your project still expects a requirements.txt (an old deployment script, a security scanner, a platform that only understands pip), uv can generate one from the lockfile without giving up the lockfile as the source of truth:
uv export --no-dev --no-hashes -o requirements.txtReplacing pyenv with uv
pyenv compiles CPython from source. That means you need a C compiler, the right development headers for OpenSSL, readline, zlib, and libffi, and roughly two to five minutes per version. When a build fails, the error is usually a missing system library, three layers deep in a make log.
uv skips all of that. It downloads prebuilt standalone CPython distributions, so installing an interpreter is a download and an extract, typically a few seconds. Nothing is compiled, nothing touches your system Python, and the interpreters live in uv's own directory rather than being wired into your shell through a shim.
# See what is available and what you already have
uv python list
# Install specific versions
uv python install 3.11 3.12 3.13
# Pin this project (writes .python-version)
uv python pin 3.12
# Run a one-off command against a different interpreter
uv run --python 3.11 pytestThere are two version declarations in a uv project and they answer different questions. .python-version says which interpreter this working copy should use right now. requires-python in pyproject.toml says which Python versions your code supports, and it constrains the resolver: set requires-python = ">=3.11" and uv will refuse to lock a dependency that dropped 3.11 support.
Libraries should set a range in requires-python and test the edges. Applications can be stricter, because you control the deployment target. Either way, commit .python-version so nobody on the team is silently running a different interpreter, and so uv python install with no arguments in CI knows what to fetch.
| pyenv | uv |
|---|---|
| `pyenv install 3.12.7` | `uv python install 3.12.7` |
| `pyenv versions` | `uv python list --only-installed` |
| `pyenv local 3.12` | `uv python pin 3.12` |
| `pyenv global 3.12` | no equivalent (uv is per-project by design) |
| `pyenv uninstall 3.11` | `uv python uninstall 3.11` |
Troubleshooting the Problems You Will Actually Hit
Most uv migrations are uneventful. These are the five failures that account for nearly all of the ones that are not.
No solution found when resolving dependencies
This is the most common shock for teams coming from pip, and it is not a uv bug. pip installs sequentially and lets a later package overwrite an earlier one's dependency, so a genuinely impossible requirement set can appear to install fine and then break at runtime. uv resolves the whole graph up front, so it tells you at install time.
uv's error output names the specific conflicting requirements and the packages that introduced them. Read it before changing anything: it usually points straight at one over-tight pin. Then work through the options in order:
- Run
uv tree --invert --package urllib3(substituting the conflicting package) to see which of your direct dependencies is imposing the constraint. - Loosen your own pins first. A dependency pinned with
==in yourpyproject.tomlthat could be>=is the cause more often than a genuine upstream conflict. - If a transitive dependency is the problem and you cannot wait for an upstream release, add an override.
[tool.uv] override-dependencies = ["urllib3>=2"]forces a version across the whole graph, ignoring what other packages requested. - Use
constraint-dependenciesinstead when you want to narrow a version range without forcing a package to be installed at all.
Private indexes and authentication
Internal package servers (Artifactory, Nexus, CodeArtifact, a private PyPI mirror) are configured in pyproject.toml rather than in a global pip config, which means the configuration is version controlled and every developer gets it automatically.
[[tool.uv.index]]
name = "internal"
url = "https://pypi.internal.example.com/simple"
explicit = true
[tool.uv.sources]
acme-billing = { index = "internal" }Setting explicit = true is the security-conscious choice. It means uv only looks at that index for packages explicitly routed to it under [tool.uv.sources], and everything else comes from PyPI. Without it, an index that serves an unexpected package name can shadow a public one, which is the shape of a dependency confusion attack.
Keep credentials out of the URL. uv reads them from environment variables named after the index, so an index named internal uses UV_INDEX_INTERNAL_USERNAME and UV_INDEX_INTERNAL_PASSWORD. In CI those come from your secrets store, and locally they can live in your shell profile or a credential helper.
Editable installs and local path dependencies
pip install -e . has a direct equivalent, and in a uv project you usually do not need it at all: uv sync installs your own package in editable mode by default. Where it matters is a sibling library you are developing alongside the application.
uv add --editable ../shared-libThat records a path source in pyproject.toml, so imports resolve to your working copy and edits take effect without reinstalling. The catch is that a path source is machine-specific: it will not work for anyone who does not have that directory checked out at the same relative location.
For anything more than a quick experiment, convert the setup into a workspace instead. Workspace members are declared once at the repository root, share a single lockfile, and work identically for every developer and in CI.
[tool.uv.workspace]
members = ["packages/*"]
[tool.uv.sources]
shared-lib = { workspace = true }uv sync keeps uninstalling a package
If you install something with uv pip install and then run uv sync, it disappears. That is correct behaviour, not a bug. uv sync makes the environment match the lockfile exactly, and anything not declared in pyproject.toml is by definition not in the lockfile.
The fix is to stop mixing interfaces. uv pip install exists as a compatibility shim for scripts and workflows that have not migrated yet; in a project with a uv.lock, use uv add. If you only want a package for a single command and not permanently, use the --with flag instead of installing anything:
uv run --with ipython ipythonHardlink warnings in Docker or on network drives
A warning about failing to hardlink files means the uv cache and the target environment are on different filesystems. This happens inside Docker with a cache mount, on some CI runners, and when a project lives on a network share or a mounted volume.
It is a performance warning rather than an error, and the fix is one environment variable: UV_LINK_MODE=copy. Set it in your Dockerfile or CI environment. You can also relocate the cache next to the environment with UV_CACHE_DIR so hardlinking works again, which is usually the faster option on a build machine you control.
When uv Is Not the Right Call
Honest limitations, because every migration guide that claims none is hiding something.
- You depend on conda for non-Python packages. uv installs from PyPI. If your stack needs CUDA toolkits, compiled geospatial libraries, or an R runtime managed by conda, uv does not replace that. Most pure-Python and wheel-distributed scientific work is fine.
- Your organisation only mirrors packages in a format uv cannot reach. Check that your internal index speaks the standard PyPI simple API before committing to the migration.
- You need a task runner in `pyproject.toml`. uv does not have one. This is a deliberate scope decision, not an oversight, and a Makefile covers it.
- Your team cannot absorb a resolver that is stricter than pip. If your dependency graph has real conflicts that pip has been papering over, the migration surfaces them all at once. That is a good outcome, but budget time for it rather than doing it the day before a release.
For everything else, the migration is genuinely a couple of hours for a typical service and the payback is immediate: faster CI, faster Docker builds, one tool instead of four, and a lockfile that means the same thing on every machine. Once uv is in place, the rest of your Python tooling gets easier to modernise too, whether that is adopting ruff or moving onto newer language features like the ones in t-strings.
Frequently Asked Questions
How much faster is uv than pip?
In benchmarks, uv installs packages 10 to 100 times faster than pip on cold cache and near-instant on warm cache. The difference is most noticeable in CI where the cache is cold on every run.
| Scenario | pip | uv |
|---|---|---|
| Install Django cold | ~45 seconds | ~3 seconds |
| Install Django warm | ~8 seconds | <1 second |
| Full data science stack cold | ~4 minutes | ~15 seconds |
Should I commit uv.lock to my repository?
Yes, always commit uv.lock. It records the exact resolved versions of every dependency (including transitive ones). Without it, two developers running uv sync at different times might install different patch versions of a dependency, leading to 'works on my machine' bugs.
Does uv support monorepos and workspaces?
Yes. uv supports workspaces similar to Cargo (Rust) and npm workspaces. Define workspace members in your root pyproject.toml:
[tool.uv.workspace]
members = ["packages/*"]Each member has its own pyproject.toml, but they share a single uv.lock at the workspace root. Dependencies are resolved together, preventing version conflicts across packages.
How do I add a dev-only dependency with uv?
Use the --dev flag:
uv add --dev pytest ruff mypyThis adds the package to the [dependency-groups] dev section of pyproject.toml rather than the main [project.dependencies]. Dev dependencies are installed with uv sync (default) but excluded when you do uv sync --no-dev for production deploys.
Can uv replace pipx for globally installed tools?
Yes. Use uv tool install to install CLI tools globally:
uv tool install ruff
uv tool install black
uv tool install httpieInstalled tools are isolated from each other and from your projects. Use uvx tool-name to run a tool once without installing it permanently: the equivalent of npx in the JavaScript ecosystem.
Can I use uv and pip in the same project during migration?
You can, and for a transition period it is the pragmatic choice. uv pip install, uv pip compile, and uv pip sync mirror the pip and pip-tools interfaces closely enough that most existing scripts work by prefixing them with uv. That alone gets you the speed win with almost no risk.
What you should not do is mix uv pip install with uv sync in a project that already has a uv.lock. The two interfaces have different ideas about what owns the environment: uv sync will remove anything that is not in the lockfile. Pick one per project and finish the migration rather than living in both.
How do I generate a requirements.txt from uv?
Use uv export, which reads the lockfile rather than the installed environment, so the output is reproducible:
# Runtime dependencies only, no hashes
uv export --no-dev --no-hashes -o requirements.txtDrop --no-hashes if the consumer supports hash verification, which you generally want for anything that installs into production. Regenerate the file in CI instead of committing it, so it can never disagree with uv.lock.
Does uv replace conda for data science work?
Partly. uv installs from PyPI, so it handles anything distributed as a wheel, which now includes numpy, pandas, scikit-learn, PyTorch, and most of the stack that historically forced people onto conda. For those projects uv is faster and simpler.
It does not replace conda when you need conda to manage non-Python system packages: CUDA toolkits, GDAL and its C dependencies, MKL builds, or an environment that mixes Python with R. conda is a general-purpose binary package manager and uv is deliberately not trying to be one.
How do I upgrade a single dependency without touching the rest?
Re-lock just that package, then sync:
uv lock --upgrade-package django
uv syncuv re-resolves django and anything it strictly requires, and leaves every other pin in the lockfile untouched. This is what you want for a security patch: the resulting diff is small enough to review, and the blast radius of the change is visible in the pull request.
uv lock --upgrade with no package name upgrades everything within the constraints in pyproject.toml. Save that for a deliberate maintenance window with the test suite in front of you, not for a Friday afternoon.
Related Articles
Husky + Prettier + lint-staged Setup for Next.js
Set up Husky v9, Prettier, and lint-staged in your Next.js project. Step-by-step guide covering pre-commit hooks with the correct 2026 config.
Drizzle ORM Migrations: A Practical drizzle-kit Guide
Learn the full Drizzle ORM migration workflow: push vs migrate, drizzle-kit setup, Turso/libSQL config, team conflicts, and production best practices.