Many use cases can be covered just by simple uses of nixpkgs, but it's often alluring to go beyond that. e.g

package manager (whether on Darwin or any linux distro), to have some tools globally available:

nix-env -iA nixpkgs.${some-package}

per project, most shell.nix can just look like this:

{
pkgs ? import <nixpkgs> {},
}:
let
some_var = some_value;
in pkgs.mkShell {
buildInputs = [
pkgs.some_package
...
];

shellHook = ''
# this is bash, so, whatever floats your boat
'''

e.g asdf, only much more generic with full non-leaking package management:

{
pkgs ? import <nixpkgs> {},
}:
let
some_package = pkgs.some_package_1_2;
in pkgs.mkShell {
buildInputs = [
some_package
pkgs.some_other_package
...
];

shellHook = ''
export SOME_VAR="some_value"
...
'';
}

ruby/rvm/rbenv/bundle exec (example for rails >= 6):

{
pkgs ? import <nixpkgs> {},
}:
let
ruby = pkgs.ruby_2_7;
python = pkgs.python27;
node = pkgs.nodejs-14_x;
in pkgs.mkShell {
buildInputs = [
ruby
pkgs.sqlite
python
node
pkgs.nodePackages.yarn
];

shellHook = ''
export RUBY_VERSION="$(ruby -e 'puts RUBY_VERSION.gsub(/\d+$/, "0")')"
export GEM_HOME="$(pwd)/vendor/bundle/ruby/$RUBY_VERSION"
export BUNDLE_PATH="$(pwd)/vendor/bundle"
export PATH="$GEM_HOME/bin:$PATH"
'';
}

python/pyenv/venv:

{
pkgs ? import <nixpkgs> {},
}:
let
python_packages = python-packages: [
python-packages.pip
];
python = pkgs.python38.withPackages python_packages;
in pkgs.mkShell {
buildInputs = [
python
];

shellHook = ''
export PYTHON_VERSION="$(python -c 'import platform; import re; print(re.sub(r"\.\d+$", "", platform.python_version()))')"
export PIP_PREFIX="$PWD/vendor/python/$PYTHON_VERSION/packages"
export PYTHONPATH="$PIP_PREFIX/lib/python$PYTHON_VERSION/site-packages:$PYTHONPATH"
unset SOURCE_DATE_EPOCH
export PATH="$PIP_PREFIX/bin:$PATH"
'';
}

mixing arm64 and x86_64 on an Apple Silicon machine:

{
x86_64 ? import <nixpkgs> { localSystem = "aarch64-darwin"; },
aarch64 ? import <nixpkgs> { localSystem = "x86_64-darwin"; }
}:
let
foo = aarch64.foo;
in aarch64.mkShell { # this makes nix-shell drop to an arm64 shell, change it to x86_64 to be intel/Rosetta2
buildInputs = [
foo
x86_64.bar
aarch64.baz
];
}

using an unstable/pinned/git package:

{
stable ? import <nixpkgs> {},
unstable ? import (fetchTarball http://nixos.org/channels/nixos-unstable/nixexprs.tar.xz) {},
pinned ? import (fetchTarball https://github.com/nixos/nixpkgs/archive/ca2ba44cab47767c8127d1c8633e2b581644eb8f.tar.gz) {},
git ? import (fetchGit { url = "https://github.com/nixos/nixpkgs/"; ref = "refs/heads/nixos-unstable"; rev = "ca2ba44cab47767c8127d1c8633e2b581644eb8f"; }) {},
}:
let
foo = stable.foo;
in stable.mkShell {
buildInputs = [
foo
unstable.bar
pinned.baz
git.qux
];
}

selecting a particular C/C++ compiler&stdlibc++ version:

{
pkgs ? import <nixpkgs> {},
}:
let
clang = pkgs.clang_12
in pkgs.llvmPackages_12.stdenv.mkDerivation {
buildInputs = [
clang
];
}

The trap is: You could handle all of that by using or writing nix features. You could even use NixOS instead of whatever distro you're used to. But then by going cold-turkey you have to learn whatever nix feature on top of all the basic (as in fundamental) things nix has to offer. Purists would say "no no no this is not the sanctioned way", which is kind of true but also setting yourself up for failure; it'd be like looking at a mountain and trying to jump right to the top, which of course you will fail to, when you could just be climbing it and be successful. Whatever practical gets you on board is fine. You can get to the "pure nix" stuff later, if you ever need to.